refactor: 에러 코드 문구 해요체로 통일
세 플랫폼이 공유하는 에러 코드 맵의 문장을 딱딱한 합니다체에서 해요체로 바꿨다. 화면 문구는 실제로 쓰는 말로만 쓴다는 기준에 맞춘 것이다. - SYS_001 "일시적인 시스템 오류가 발생했습니다." → "일시적인 오류가 생겼어요." - AUTH_003 "해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다." → "이 메뉴나 기능을 이용할 권한이 없어요." - VAL_001 의 "(필수값 누락 또는 형식 오류)" 는 개발자 말이라 문장으로 풀었다. - main.ts 의 Rate Limit 응답도 같은 문장을 쓰고 있어 "요청이 너무 많아요" 로 함께 고쳤다. SSOT 3개 파일(backend 필터 · frontend ErrorCodeLexicon · flutter ErrorLexicon)과 규칙 문서, 그리고 문구를 인용하던 화면·테스트를 함께 갱신했다. frontend lint · type-check · 테스트 11개 통과. flutter analyze 에서 error_lexicon.dart 는 문제 없음. backend 는 node_modules 가 비어 있어 로컬에서 lint/test 를 돌리지 못했다(문자열 리터럴만 변경). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0128kgT8ycCATfJVFULQMa9b
This commit is contained in:
@@ -13,13 +13,13 @@ description: 세 플랫폼이 공유하는 에러 코드 ↔ 한국어 메시지
|
||||
|
||||
| 코드 | HTTP / 상황 | 메시지 |
|
||||
| -------- | -------------------- | ------------------------------------------------------- |
|
||||
| SYS_001 | 500 / 미정의 예외 | "일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." |
|
||||
| AUTH_001 | 401 | "로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요." |
|
||||
| AUTH_002 | 세션 만료 (클라이언트) | "안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요." |
|
||||
| AUTH_003 | 403 | "해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다." |
|
||||
| VAL_001 | 400 / 422 (Validation) | "입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)" |
|
||||
| RES_001 | 404 | "요청하신 정보나 페이지를 찾을 수 없습니다." |
|
||||
| BIZ_001 | 200(비즈니스 실패) / 기타 | "요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요." |
|
||||
| SYS_001 | 500 / 미정의 예외 | "일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요." |
|
||||
| AUTH_001 | 401 | "로그인이 필요한 기능이에요. 로그인 후 이용해 주세요." |
|
||||
| AUTH_002 | 세션 만료 (클라이언트) | "안전을 위해 로그아웃했어요. 다시 로그인해 주세요." |
|
||||
| AUTH_003 | 403 | "이 메뉴나 기능을 이용할 권한이 없어요." |
|
||||
| VAL_001 | 400 / 422 (Validation) | "입력하신 내용을 다시 확인해 주세요. 빠진 항목이 있거나 형식이 맞지 않아요." |
|
||||
| RES_001 | 404 | "찾으시는 정보나 페이지가 없어요." |
|
||||
| BIZ_001 | 200(비즈니스 실패) / 기타 | "요청하신 작업을 끝내지 못했어요. 다시 시도해 주세요." |
|
||||
|
||||
> 비즈니스 예외는 `HttpException` 응답의 `message` 로 사용자 노출 문구를 재정의할 수 있다.
|
||||
> 응답 래퍼 구조는 [[response-format]] 참조.
|
||||
|
||||
@@ -21,7 +21,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let code = 'SYS_001';
|
||||
let message =
|
||||
'일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';
|
||||
'일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요.';
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
@@ -29,23 +29,23 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
|
||||
if (status === HttpStatus.UNAUTHORIZED) {
|
||||
code = 'AUTH_001';
|
||||
message = '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.';
|
||||
message = '로그인이 필요한 기능이에요. 로그인 후 이용해 주세요.';
|
||||
} else if (status === HttpStatus.FORBIDDEN) {
|
||||
code = 'AUTH_003';
|
||||
message = '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.';
|
||||
message = '이 메뉴나 기능을 이용할 권한이 없어요.';
|
||||
} else if (status === HttpStatus.NOT_FOUND) {
|
||||
code = 'RES_001';
|
||||
message = '요청하신 정보나 페이지를 찾을 수 없습니다.';
|
||||
message = '찾으시는 정보나 페이지가 없어요.';
|
||||
} else if (
|
||||
status === HttpStatus.BAD_REQUEST ||
|
||||
status === HttpStatus.UNPROCESSABLE_ENTITY
|
||||
) {
|
||||
code = 'VAL_001';
|
||||
message =
|
||||
'입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
|
||||
'입력하신 내용을 다시 확인해 주세요. 빠진 항목이 있거나 형식이 맞지 않아요.';
|
||||
} else {
|
||||
code = 'BIZ_001';
|
||||
message = '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.';
|
||||
message = '요청하신 작업을 끝내지 못했어요. 다시 시도해 주세요.';
|
||||
|
||||
// 비즈니스 예외에서는 사용자에게 보일 메시지를 재정의 가능
|
||||
if (
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ async function bootstrap() {
|
||||
error: {
|
||||
code: 'SYS_001',
|
||||
message:
|
||||
'일시적인 시스템 오류가 발생했습니다. (요청 한도 초과) 잠시 후 다시 시도해 주세요.', // 글로벌 통합 에러 포맷
|
||||
'요청이 너무 많아요. 잠시 후 다시 시도해 주세요.', // 글로벌 통합 에러 포맷
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
class ErrorLexicon {
|
||||
static const Map<String, String> codes = {
|
||||
'SYS_001': '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
'AUTH_001': '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.',
|
||||
'AUTH_002': '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.',
|
||||
'AUTH_003': '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.',
|
||||
'VAL_001': '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)',
|
||||
'RES_001': '요청하신 정보나 페이지를 찾을 수 없습니다.',
|
||||
'BIZ_001': '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.',
|
||||
'SYS_001': '일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요.',
|
||||
'AUTH_001': '로그인이 필요한 기능이에요. 로그인 후 이용해 주세요.',
|
||||
'AUTH_002': '안전을 위해 로그아웃했어요. 다시 로그인해 주세요.',
|
||||
'AUTH_003': '이 메뉴나 기능을 이용할 권한이 없어요.',
|
||||
'VAL_001': '입력하신 내용을 다시 확인해 주세요. 빠진 항목이 있거나 형식이 맞지 않아요.',
|
||||
'RES_001': '찾으시는 정보나 페이지가 없어요.',
|
||||
'BIZ_001': '요청하신 작업을 끝내지 못했어요. 다시 시도해 주세요.',
|
||||
};
|
||||
|
||||
static String getMessage(String code) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 섹션 본문의 상태 분기 — 로딩(스켈레톤) · 실패 · 비어 있음 · 정상.
|
||||
// 뿌리가 상태마다 갈리는 프래그먼트라 이 컴포넌트에 class 를 넘겨도 DOM 에 닿지 않는다.
|
||||
// 여백·테두리는 부르는 쪽에서 감싸개 요소에 준다.
|
||||
// 섹션마다 이 네 상태를 모두 가진다. 한 섹션이 실패해도 그 섹션만 다시 부른다.
|
||||
import SectionStateBlock from '@/components/common/SectionStateBlock.vue'
|
||||
import type { IconName } from '@/components/common/app-icon'
|
||||
@@ -19,7 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
errorMessage: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
errorMessage: '일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요.',
|
||||
minHeight: '15rem',
|
||||
emptyDescription: undefined,
|
||||
emptyLinkLabel: undefined,
|
||||
|
||||
@@ -6,24 +6,24 @@ describe('resolveErrorMessage', () => {
|
||||
it('서버가 내려준 에러 코드의 문장을 쓴다', () => {
|
||||
const error = { response: { status: 400, data: { success: false, error: { code: 'AUTH_003', message: '' } } } }
|
||||
|
||||
expect(resolveErrorMessage(error)).toBe('해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.')
|
||||
expect(resolveErrorMessage(error)).toBe('이 메뉴나 기능을 이용할 권한이 없어요.')
|
||||
})
|
||||
|
||||
it('에러 코드가 없으면 HTTP 상태로 코드를 정한다', () => {
|
||||
expect(resolveErrorMessage({ response: { status: 401 } })).toBe(
|
||||
'로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.',
|
||||
'로그인이 필요한 기능이에요. 로그인 후 이용해 주세요.',
|
||||
)
|
||||
expect(resolveErrorMessage({ response: { status: 422 } })).toBe(
|
||||
'입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)',
|
||||
'입력하신 내용을 다시 확인해 주세요. 빠진 항목이 있거나 형식이 맞지 않아요.',
|
||||
)
|
||||
expect(resolveErrorMessage({ response: { status: 404 } })).toBe(
|
||||
'요청하신 정보나 페이지를 찾을 수 없습니다.',
|
||||
'찾으시는 정보나 페이지가 없어요.',
|
||||
)
|
||||
})
|
||||
|
||||
it('모르는 코드나 응답이 없는 에러는 SYS_001 문장으로 떨어진다', () => {
|
||||
const unknownCode = { response: { status: 500, data: { success: false, error: { code: 'NOPE_999', message: '' } } } }
|
||||
const fallbackMessage = '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'
|
||||
const fallbackMessage = '일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요.'
|
||||
|
||||
expect(resolveErrorMessage(unknownCode)).toBe(fallbackMessage)
|
||||
expect(resolveErrorMessage(new Error('네트워크 끊김'))).toBe(fallbackMessage)
|
||||
|
||||
@@ -25,13 +25,13 @@ declare module 'axios' {
|
||||
// 에러 코드 사전 — CLAUDE.md / 백엔드 HttpExceptionFilter 와 1:1 매핑
|
||||
// SYS_001 은 폴백 메시지로 항상 존재함을 타입으로 보장 (noUncheckedIndexedAccess 대응)
|
||||
const ErrorCodeLexicon: Record<string, string> & { SYS_001: string } = {
|
||||
SYS_001: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
AUTH_001: '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.',
|
||||
AUTH_002: '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.',
|
||||
AUTH_003: '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.',
|
||||
VAL_001: '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)',
|
||||
RES_001: '요청하신 정보나 페이지를 찾을 수 없습니다.',
|
||||
BIZ_001: '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.',
|
||||
SYS_001: '일시적인 오류가 생겼어요. 잠시 후 다시 시도해 주세요.',
|
||||
AUTH_001: '로그인이 필요한 기능이에요. 로그인 후 이용해 주세요.',
|
||||
AUTH_002: '안전을 위해 로그아웃했어요. 다시 로그인해 주세요.',
|
||||
AUTH_003: '이 메뉴나 기능을 이용할 권한이 없어요.',
|
||||
VAL_001: '입력하신 내용을 다시 확인해 주세요. 빠진 항목이 있거나 형식이 맞지 않아요.',
|
||||
RES_001: '찾으시는 정보나 페이지가 없어요.',
|
||||
BIZ_001: '요청하신 작업을 끝내지 못했어요. 다시 시도해 주세요.',
|
||||
}
|
||||
|
||||
// 사용자 노출용 알림 — Toast 라이브러리 도입 시 이 함수만 교체하면 됨
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('useSectionResource', () => {
|
||||
await resource.load()
|
||||
|
||||
expect(resource.status.value).toBe('error')
|
||||
expect(resource.errorMessage.value).toBe('요청하신 정보나 페이지를 찾을 수 없습니다.')
|
||||
expect(resource.errorMessage.value).toBe('찾으시는 정보나 페이지가 없어요.')
|
||||
expect(resource.data.value).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
role="alert"
|
||||
>
|
||||
<h1>404</h1>
|
||||
<p>요청하신 정보나 페이지를 찾을 수 없습니다.</p>
|
||||
<p>찾으시는 정보나 페이지가 없어요.</p>
|
||||
<RouterLink to="/">
|
||||
홈으로 돌아가기
|
||||
</RouterLink>
|
||||
|
||||
Reference in New Issue
Block a user