도메인 개념(User, Session, RAG)의 타입·상수·순수 함수. UI/API/사이드이펙트 금지.
상위: ../../CLAUDE.md
각 도메인 슬라이스는 다음을 제공:
- 타입 정의: 백엔드 응답 타입을 도메인 모델로 변환한 형태
- 상수: ENUM 값, 라벨, 색상 매핑
- 순수 함수: 점수 계산, 상태 전이 가능 여부 판단, 표시 포맷
- 타입 가드 / 변환 함수
비책임: React 컴포넌트, API 호출, 사이드이펙트.
domain/{name}/
├── model/ # 타입 정의 (zod schema, type aliases)
├── constants/ # ENUM, 라벨 매핑
├── lib/ # 순수 함수
└── index.ts # public API
| 도메인 | 핵심 타입 | 위치 |
|---|---|---|
user |
User, UserConsent, JobCategory |
domain/user |
session |
InterviewSession, SessionStatus, InterviewType, Message, MessageRole |
domain/session |
rag |
AnalyzedDocument, DocStatus, AnalysisState |
domain/rag |
// domain/session/model/types.ts
export const SESSION_STATUS = {
READY: 'READY',
IN_PROGRESS: 'IN_PROGRESS',
INTERRUPTED: 'INTERRUPTED',
COMPLETED: 'COMPLETED',
CANCELLED: 'CANCELLED',
} as const;
export type SessionStatus = typeof SESSION_STATUS[keyof typeof SESSION_STATUS];→ 백엔드 ENUM 변경 시 본 파일 수동 동기화 (또는 OpenAPI 자동 생성 결과를 그대로 re-export).
// domain/session/constants/labels.ts
export const SESSION_STATUS_LABEL: Record<SessionStatus, string> = {
READY: '대기 중',
IN_PROGRESS: '진행 중',
INTERRUPTED: '중단됨',
COMPLETED: '완료',
CANCELLED: '취소됨',
};디자인 시스템의 status 색상은 domain/{name}/constants/colors.ts에 매핑:
export const SESSION_STATUS_COLOR: Record<SessionStatus, BadgeVariant> = {
READY: 'neutral',
IN_PROGRESS: 'warning',
INTERRUPTED: 'warning',
COMPLETED: 'success',
CANCELLED: 'disabled',
};shared/ui/StatusBadge 같은 컴포넌트가 이 매핑을 사용.
// domain/session/lib/transitions.ts
export function canStart(session: InterviewSession): boolean {
return session.status === 'READY';
}
export function canEnd(session: InterviewSession): boolean {
return session.status === 'IN_PROGRESS';
}
export function reachedLimit(session: InterviewSession): boolean {
return session.totalQuestionCount >= session.maxQuestions;
}이런 로직을 컴포넌트나 훅에 흩지 않는다 — domain에 모은다.
domain/* → shared/* ✓
domain/* → domain/* ⚠️ 단방향만 ✓, 순환 ✗
domain/* → features/* ✗
domain/* → pages/*, app/* ✗
타입 자체는 다른 도메인을 참조 가능 (예: Session이 User를 알고 있음).
도메인 간 의존성은 단방향 그래프(DAG) 여야 한다. 순환이 발생하면 타입 추론 무한 루프, 모듈 번들 분리 불가, 테스트 시 mock 지옥 등 실질적 피해가 크다.
나쁜 예 (순환):
// domain/session/model/types.ts
import type { User } from '@/domain/user';
export type Session = { user: User; ... };
// domain/user/model/types.ts
import type { Session } from '@/domain/session'; // ❌ 순환!
export type User = { recentSessions: Session[]; ... };좋은 예 (단방향):
// domain/user (의존 없음, 가장 아래)
export type User = { id: number; githubUsername: string; ... };
// domain/session (user에 의존)
import type { User } from '@/domain/user';
export type Session = { userId: User['id']; ... }; // ID만 보유
// "user의 최근 세션" 같은 derived view는 features/history에서 조합session → user
rag → user
새 도메인 추가 시 본 그래프를 갱신하고, 위쪽에 위치한 도메인이 아래쪽을 import하는 방향만 허용.
ESLint no-restricted-imports 또는 eslint-plugin-boundaries 도입 시:
// eslint.config.js (예시)
{
rules: {
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{ from: 'domain-user', allow: ['shared'] },
{ from: 'domain-session', allow: ['shared', 'domain-user'] },
{ from: 'domain-rag', allow: ['shared', 'domain-user'] },
],
}],
},
}CI 단계에서 자동으로 위반 차단. 백엔드는 ArchUnit 사용 (/backend/CLAUDE.md).
서버 응답을 런타임 검증하고 싶다면:
import { z } from 'zod';
export const SessionSchema = z.object({
id: z.number(),
title: z.string().nullable(),
status: z.enum([...Object.values(SESSION_STATUS)]),
// ...
});
export type Session = z.infer<typeof SessionSchema>;API 응답 boundary에서만 검증 (호출 시마다 검증하면 비용).
- ❌ React 컴포넌트, 훅 (UI는 features/shared/ui)
- ❌ axios·fetch 호출
- ❌ 글로벌 변수, mutable state
- ❌
Date.now(),Math.random()같은 비결정적 호출 (테스트 불가) - ❌ 백엔드 응답 그대로 노출 (필요하면 도메인 타입으로 변환)
- ❌ 도메인 간 순환 import — 발견 즉시 ID 참조로 분해 (§6.1)
- ❌ 한 도메인이 너무 많은 다른 도메인 import — 책임 경계 재검토 필요