@@ -125,10 +125,11 @@ export const getArchive = async (params: GetArchiveParams) => {
125125
126126### 컴포넌트 구성
127127
128- - ` src/components/ui/ ` — shadcn/ui 기본 컴포넌트 (Button, Input, Dialog 등). 스타일: ` new-york ` . ` components.json ` 에서 설정.
129- - ` src/components/layout/ ` — Header, Sidebar, Footer
130- - ` src/components/modals/ ` , ` cards/ ` , ` lists/ ` , ` payment/ ` , ` premium/ ` — 도메인별 그룹화된 컴포넌트
131- - ` src/features/ ` , ` src/entities/ ` , ` src/widgets/ ` — 부분적 FSD (Feature-Sliced Design) 구조, 향후 타입 기반 구조로 통합 예정
128+ - 공용 UI는 주로 ` src/components/common/ui/ ` 아래에 위치한다. 예: ` Button ` , ` Dialog ` , ` Toast ` , ` FloatingInquiryButton `
129+ - 공용 레이아웃은 ` src/components/common/layout/ ` 아래에 위치한다. 예: ` Header ` , ` AdminSideBar `
130+ - 공용 모달은 ` src/components/common/modals/ ` 아래에 위치한다
131+ - 페이지 단위 조합 컴포넌트는 ` src/components/pages/ ` , 도메인별 조합은 ` payment/ ` , ` discussion/ ` , ` archive/ ` , ` balance-game/ ` , ` mentoring ` 관련 디렉토리 등으로 분산되어 있다
132+ - ` src/features/ ` 기반 구조와 전통적인 ` components/ ` , ` hooks/queries/ ` 구조가 공존한다. 신규 변경 시 xkdl 한 PR 안에서 구조를 섞어 바꾸지 않는다
132133
133134### 스타일링
134135
@@ -145,6 +146,122 @@ export const getArchive = async (params: GetArchiveParams) => {
1451463 . Axios 인터셉터가 ` AUTH001 ` 에러 감지 → 토큰 갱신 → 실패한 요청 재시도 (중복 갱신 방지를 위한 큐 사용)
1461474 . 미들웨어가 서버 측에서 네비게이션 시 토큰 검증, 유효하지 않으면 ` / ` 로 리다이렉트
147148
149+ ### 에러 핸들링
150+
151+ 에러 처리는 ` src/utils/error-handler.ts ` 를 중심으로 중앙 집중식 관리한다. ` src/utils/error.ts ` 는 ` extractErrorCode() ` 하위 호환성용 deprecated 래퍼다.
152+
153+ #### 핵심 파일
154+
155+ - ` src/utils/error-handler.ts ` — ` analyzeError() ` , ` logError() ` , ` ErrorType ` , ` ErrorInfo ` . 에러 코드-메시지 매핑(~ 40개), 한국어 fallback, Sentry 보고를 모두 담당
156+ - ` src/config/query-client.ts ` — ` MutationCache ` 글로벌 에러 핸들러. ` onError ` 가 없는 mutation 실패 시 자동으로 에러 toast + Sentry 보고
157+ - ` src/app/(service)/error.tsx ` , ` (landing)/error.tsx ` , ` (admin)/error.tsx ` — route segment 에러 경계
158+ - ` src/app/global-error.tsx ` — root 에러 경계 (Sentry 자동 캡처)
159+ - ` src/app/not-found.tsx ` 및 각 route group의 ` not-found.tsx `
160+
161+ #### 에러 분류 체계
162+
163+ ` analyzeError() ` 는 3가지 에러 타입을 순서대로 분류한다:
164+
165+ 1 . ** AxiosError** — ` isAxiosError() ` 통과. HTTP 상태 코드 + API 에러 응답 추출
166+ 2 . ** ApiError** — axios 인터셉터가 변환한 커스텀 에러. ` isApiError() ` 타입 가드로 ` errorCode ` , ` statusCode ` 보존
167+ 3 . ** 일반 Error / unknown** — fallback 처리
168+
169+ ```
170+ AxiosError → isAxiosError() ✅ → HTTP 상태/에러 코드 추출
171+ ApiError → isApiError() ✅ → errorCode/statusCode 보존 (인터셉터 변환 에러)
172+ Error → instanceof Error → UNKNOWN 타입
173+ unknown → String(error) → 기본 메시지
174+ ```
175+
176+ #### 에러 코드-메시지 매핑
177+
178+ ` error-handler.ts ` 의 ` codeMessages ` 객체에서 중앙 관리. 에러 코드 prefix별 분류:
179+
180+ | Prefix | 도메인 | 예시 |
181+ | --------| --------| ------|
182+ | AUTH | 인증 | AUTH001(토큰 만료), AUTH002(권한 없음) |
183+ | CMM | 공통 | CMM001(입력값 오류), CMM006(접근 권한) |
184+ | MEM | 회원 | MEM002(회원 미존재), MEM003(중복 가입) |
185+ | GSM/GSA | 스터디 관리/신청 | GSM001(스터디 미존재), GSA003(정원 초과) |
186+ | HWK/EVL | 과제/평가 | HWK003(제출 기간 만료), EVL002(중복 평가) |
187+ | PAY 2xx | 결제 | PAY202(중복 승인), PAY207(금액 불일치) |
188+ | PAY 3xx | 환불 | PAY302(중복 환불), PAY307(환불 불가) |
189+ | FILE | 파일 | FILE001(업로드 실패), FILE002(형식 미지원) |
190+
191+ 매핑에 없는 코드는 백엔드 ` message ` 가 한국어이면 그대로 사용(` /[가-힣]/ ` 정규식). 에러 코드를 사용자에게 직접 노출하지 않는다.
192+
193+ #### Mutation 에러 글로벌 핸들러
194+
195+ ` query-client.ts ` 의 ` MutationCache.onError ` 가 안전망 역할:
196+
197+ - ` onError ` 핸들러가 없는 mutation 실패 시 자동으로 에러 toast 표시 + Sentry 보고
198+ - 개별 ` onError ` 가 있으면 글로벌 핸들러 스킵 (중복 방지)
199+ - Query 에러는 글로벌 핸들러 미적용 (다중 동시 실패 시 toast 폭주 방지)
200+
201+ #### 클라이언트 에러 처리 원칙
202+
203+ - 복구 가능한 실패 (` recoverable ` ): 사용자 흐름을 유지한다. Inline error를 우선하고, Toast를 보조적으로 사용한다. ** 브라우저 시스템 ` alert() ` 는 사용하지 않는다** — Toast(` useToastStore ` )를 사용한다
204+ - 사용자 판단이 필요한 실패 (` action required ` ): 다음 행동을 선택해야 할 때 Modal 또는 앱 내 확인 UI를 사용한다. 브라우저 시스템 ` alert() ` 는 사용하지 않으며 기존의 디자인 시스템을 활용한다
205+ - 치명적 실패 (` fatal ` , page-level): 특정 화면이 더 이상 정상 동작할 수 없을 때 route segment의 ` error.tsx ` 또는 client error boundary를 사용한다
206+ - 애플리케이션 전체 실패 (` critical ` , app-level): hydration mismatch, 인증 컨텍스트 붕괴, 전역 provider 오류처럼 앱 전체에 영향을 주는 경우 ` global-error.tsx ` 가 잡고 Sentry로 자동 보고
207+
208+ Toast 사용 패턴:
209+
210+ ``` typescript
211+ // 컴포넌트 내부 (React hook 사용)
212+ const showToast = useToastStore ((state ) => state .showToast );
213+ showToast (' 환불 요청이 접수되었습니다.' , ' success' );
214+
215+ // Hook / React 외부 (getState 사용)
216+ useToastStore .getState ().showToast (errorInfo .userMessage , ' error' );
217+ ```
218+
219+ ` <GlobalToast /> ` 는 ` (service) ` , ` (landing) ` , ` (admin) ` 세 레이아웃 모두에 마운트되어 있다.
220+
221+ #### 서버 에러 처리 원칙
222+
223+ - SSR/Server Component에서 필수 데이터 로딩에 실패해 페이지가 성립하지 않으면 예외를 다시 던져 ` error.tsx ` 로 보낸다
224+ - 리소스가 존재하지 않는 케이스는 ` notFound() ` 로 분기한다
225+ - ` fetchQuery() ` / ` prefetchQuery() ` 의 ` queryFn ` 은 ` undefined ` 를 반환하면 안 된다. 404는 ` notFound() ` , 그 외는 반드시 ` throw error `
226+
227+ 예: ` src/api/endpoints/group-study/get-group-study-detail.server.ts ` 는 ` GSM001 ` 이면 ` notFound() ` , 나머지 에러는 ` throw error ` 한다
228+
229+ ``` typescript
230+ export default async function Page() {
231+ const data = await fetchData ();
232+ return <PageView data ={data} />;
233+ }
234+ ```
235+
236+ 불필요한 ` try/catch ` 로 에러를 삼켜 ` undefined ` 를 반환하지 않는다.
237+
238+ #### 운영 보안 원칙
239+
240+ - Production에서는 ` stack trace ` , 원문 서버 메시지, 내부 경로, 민감한 백엔드 응답을 사용자 화면에 직접 노출하지 않는다
241+ - 3개 ` error.tsx ` 모두 ` process.env.NODE_ENV === 'development' ` 게이팅 적용: technicalMessage, error.message, error.stack은 개발 환경에서만 표시
242+ - 사용자 화면에는 일반화된 ` userMessage ` , 필요 시 ` errorCode ` , ` statusCode ` , ` digest ` 정도만 노출한다
243+ - ` digest ` 는 서버 로그 또는 Sentry에서 원인을 찾기 위한 추적용 식별자로 사용한다
244+ - API route에서도 production 응답에는 상세 ` details ` 를 그대로 넣지 않는 방향을 기본 원칙으로 삼는다
245+
246+ #### 성공 페이지 원칙
247+
248+ - 스터디 생성, 스터디 참여, 결제 완료 같은 주요 성공 이벤트는 별도 success page 또는 완료 화면으로 사용자의 다음 행동을 명확히 안내한다
249+ - 브랜딩 요소는 환영 문구, 운영팀 메시지, 후속 행동 CTA 중심으로 넣고, 정보량이 많은 경우에도 핵심 CTA를 먼저 보이게 한다
250+
251+ #### 모니터링 (Sentry)
252+
253+ ` @sentry/nextjs ` 가 통합되어 있다. 에러는 ` logError() ` → ` Sentry.captureException() ` 경로로 자동 보고된다.
254+
255+ - ** SDK 설정 파일** : ` sentry.client.config.ts ` , ` sentry.server.config.ts ` , ` sentry.edge.config.ts ` (프로젝트 루트)
256+ - ** Next.js instrumentation** : ` src/instrumentation.ts ` — 서버/엣지 런타임 초기화 + ` onRequestError ` 자동 캡처
257+ - ** next.config.ts** : ` withSentryConfig() ` 래핑 — 소스맵 업로드, 트리셰이킹
258+ - ** 환경 변수** : ` NEXT_PUBLIC_SENTRY_DSN ` (런타임), ` SENTRY_ORG ` / ` SENTRY_PROJECT ` / ` SENTRY_AUTH_TOKEN ` (CI 소스맵 업로드)
259+ - ** 환경 감지** : ` NEXT_PUBLIC_API_BASE_URL ` 기반으로 ` production ` / ` staging ` / ` development ` 자동 분류
260+ - ** 필터링** : AUTH001(토큰 만료)은 정상 플로우이므로 ` beforeSend ` 에서 Sentry 보고 제외
261+ - ** 성능** : ` tracesSampleRate: 0.1 ` (10%), Session Replay는 에러 시에만 기록 (` replaysOnErrorSampleRate: 1.0 ` )
262+ - DSN이 없으면 Sentry가 초기화되지 않으므로, 로컬 개발에서는 환경 변수 없이도 정상 동작한다
263+ - 운영 단계에서는 Slack 즉시 알림을 연동할 수 있지만, 노이즈를 줄이기 위해 임계치와 대상 에러 범위를 먼저 정의한다
264+
148265### 경로 별칭
149266
150267` @/* ` 는 ` ./src/* ` 에 매핑됨 (tsconfig.json에서 설정)
@@ -173,3 +290,7 @@ export const getArchive = async (params: GetArchiveParams) => {
173290- ` NEXT_PUBLIC_TOSS_CLIENT_KEY ` — 토스페이먼츠
174291- ` NEXT_PUBLIC_CLARITY_PROJECT_ID ` — Microsoft Clarity
175292- ` NEXT_PUBLIC_GTM_ID ` — Google Tag Manager
293+ - ` NEXT_PUBLIC_SENTRY_DSN ` — Sentry DSN (없으면 Sentry 비활성화)
294+ - ` SENTRY_ORG ` — Sentry 조직 (CI 소스맵 업로드용)
295+ - ` SENTRY_PROJECT ` — Sentry 프로젝트 (CI 소스맵 업로드용)
296+ - ` SENTRY_AUTH_TOKEN ` — Sentry 인증 토큰 (CI 소스맵 업로드용)
0 commit comments