{stat.value}
+``` + +**Next.js**: +```tsx +{stat.value}
+``` + +❌ 불일치: text-3xl md:text-4xl 누락 +``` + +**최종 빌드 검증**: +```bash +npm run build +``` + +--- + +## Phase 3: 섹션 통합 검증 + +모든 섹션 완료 후: +1. [ ] 전체 페이지 빌드 성공 +2. [ ] 모든 기능 정상 동작 +3. [ ] React와 시각적 차이 없음 +4. [ ] 반응형 동작 확인 (모바일, 태블릿, 데스크톱) + +--- + +## 실전 예시: ItemManagement (2600줄) + +### 파일 분석 +``` +파일: ItemManagement.tsx +크기: 2,600줄 +전략: 섹션별 분해 (5개 섹션) +``` + +### 섹션 분해 계획 +| 섹션 | 라인 | 복잡도 | 체크포인트 | +|------|------|--------|-----------| +| Header | 1899-1917 | 낮음 | 6개 | +| StatCards | 1790-1816, 1920 | 낮음 | 8개 | +| SearchFilter | 1929-1950 | 중간 | 10개 | +| Tabs+Table | 1956-2300 | 높음 | 15개 | +| DetailView | 2300-2900 | 매우 높음 | 20개 | + +### 작업 진행 +``` +✅ 1회차: Header (6단계 완료, 검증 통과) +✅ 2회차: StatCards (6단계 완료, 검증 통과) +✅ 3회차: SearchFilter (6단계 완료, 검증 통과) +🔄 4회차: Tabs+Table (진행 중...) +⏳ 5회차: DetailView (대기 중) +``` + +--- + +## 예상되는 실수 패턴 및 방지법 + +### 실수 1: 텍스트 사이즈 불일치 +**증상**: `text-2xl` vs `text-3xl md:text-4xl` +**원인**: 체크리스트에서 반응형 클래스 누락 +**방지**: 모든 `md:`, `lg:` 클래스도 체크리스트에 명시 + +### 실수 2: font-bold 유무 +**증상**: 타이틀에 bold가 있어야 하는데 없거나, 없어야 하는데 있거나 +**원인**: 부정 체크(❌)를 체크리스트에 안 적음 +**방지**: "없어야 할 클래스"도 `font-bold ❌` 형태로 명시 + +### 실수 3: opacity, shadow 같은 미세 스타일 +**증상**: `opacity-15` vs `opacity-20`, `shadow-sm` vs `shadow-md` +**원인**: 숫자까지 정확히 확인 안 함 +**방지**: 체크리스트에 정확한 값까지 기록 + +### 실수 4: 컴포넌트 variant 불일치 +**증상**: `variant="default"` vs `variant="secondary"` +**원인**: Props도 CSS처럼 체크해야 함 +**방지**: variant, size 같은 Props도 체크리스트에 포함 + +--- + +## 워크플로우 메타 규칙 + +### 언제 이 워크플로우를 사용하는가? +1. 사용자가 "React와 똑같이" 요청 +2. 파일이 1000줄 이상 +3. 이전에 디테일을 놓친 경험이 있을 때 +4. 사용자가 "체크리스트 방식으로" 명시 + +### 언제 사용하지 않는가? +1. 간단한 버그 수정 (<50줄) +2. 새로운 기능 추가 (참조할 React 코드 없음) +3. 리팩토링 작업 +4. 사용자가 "대략적으로만" 요청 + +### 워크플로우 적용 선언 +작업 시작 시 사용자에게 명시: +``` +📋 대용량 파일 워크플로우 적용 + +파일: ItemCreate.tsx (1,200줄) +전략: 4개 섹션으로 분해 +예상 시간: 40분 + +Section 1: FormHeader (진행 중...) +``` + +--- + +--- + +## Phase 4: 복잡한 다중 작업 처리 프로토콜 + +### 개요 +사용자가 여러 요구사항을 한 번에 제시할 때 누락 없이 체계적으로 처리하는 프로세스 + +### 트리거 조건 +다음 중 하나라도 해당되면 이 프로토콜 적용: +- ✅ 3개 이상의 독립적인 수정 요청 +- ✅ 여러 파일/섹션에 걸친 작업 +- ✅ 복잡한 로직 변경 + UI 수정 혼재 +- ✅ 사용자가 "여러 개 한번에" 또는 "전체적으로" 요청 + +### Step 1: TodoWrite로 작업 분해 및 체크리스트 생성 + +**원칙**: +- 모든 요구사항을 독립적인 태스크로 분해 +- 각 태스크는 검증 가능한 단위로 작성 +- 작업 순서를 논리적으로 정렬 (의존성 고려) + +**예시 입력**: +``` +사용자: "구매부품 화면을 다음과 같이 수정해줘: +1. 품목명 선택 시 전원, 용량, 단위, 비고 표시 +2. 전동개폐기는 220V/380V 선택 +3. 모터는 용량 입력 + 전압 선택 +4. 체인은 규격 선택 + 길이 입력 +5. 품목코드 자동생성 로직 변경 +6. 품목 상태를 품목코드 다음으로 이동" +``` + +**TodoWrite 출력**: +```typescript +[ + { + content: "요구사항 분석 및 파일 구조 파악", + status: "in_progress", + activeForm: "요구사항 분석 중" + }, + { + content: "품목명 선택 로직 확인 (기존 코드)", + status: "pending", + activeForm: "품목명 선택 로직 확인 중" + }, + { + content: "전원, 용량, 단위, 비고 필드 추가", + status: "pending", + activeForm: "필드 추가 중" + }, + { + content: "전동개폐기 전용 220V/380V 선택 구현", + status: "pending", + activeForm: "전동개폐기 필드 구현 중" + }, + { + content: "모터 전용 용량 입력 + 전압 선택 구현", + status: "pending", + activeForm: "모터 필드 구현 중" + }, + { + content: "체인 전용 규격 선택 + 길이 입력 구현", + status: "pending", + activeForm: "체인 필드 구현 중" + }, + { + content: "품목코드 자동생성 함수 수정", + status: "pending", + activeForm: "품목코드 로직 수정 중" + }, + { + content: "품목 상태 기존 위치 제거", + status: "pending", + activeForm: "품목 상태 제거 중" + }, + { + content: "품목 상태 새 위치(품목코드 다음)에 추가", + status: "pending", + activeForm: "품목 상태 추가 중" + }, + { + content: "전체 동작 검증 및 빌드 테스트", + status: "pending", + activeForm: "검증 중" + } +] +``` + +### Step 2: 순차적 실행 및 진행상황 업데이트 + +**실행 규칙**: +1. **한 번에 하나씩**: 절대 여러 태스크를 동시에 처리하지 않음 +2. **완료 후 체크**: 각 태스크 완료 즉시 TodoWrite 업데이트 +3. **명시적 보고**: "✅ N번 완료, 다음 N+1번 시작" 형태로 진행상황 공유 +4. **에러 처리**: 실패 시 즉시 보고, 해결 후 재개 + +**실행 흐름**: +``` +1. Task 1 시작 + → TodoWrite: status="in_progress" + → 작업 수행 + → 검증 + → TodoWrite: status="completed" + → 사용자에게 "✅ 1번 완료" 보고 + +2. Task 2 시작 + → TodoWrite: status="in_progress" + → 작업 수행 + ... +``` + +### Step 3: 중간 체크포인트 제시 + +**5개 태스크마다 중간 보고**: +```markdown +## 진행상황 (5/10 완료) + +✅ 완료: +1. 요구사항 분석 및 파일 구조 파악 +2. 품목명 선택 로직 확인 +3. 전원, 용량, 단위, 비고 필드 추가 +4. 전동개폐기 전용 필드 구현 +5. 모터 전용 필드 구현 + +🔄 진행 중: +6. 체인 전용 필드 구현 + +⏳ 대기 중: +7. 품목코드 자동생성 함수 수정 +8. 품목 상태 기존 위치 제거 +9. 품목 상태 새 위치 추가 +10. 전체 검증 +``` + +### Step 4: 최종 완료 보고서 + +**모든 태스크 완료 후**: +```markdown +## ✅ 전체 작업 완료 + +### 완료된 작업 요약 +1. ✅ 품목명 선택 시 전원, 용량, 단위, 비고 표시 (line 990-1122) +2. ✅ 전동개폐기 220V/380V 선택 구현 (line 990-1032) +3. ✅ 모터 용량 입력 + 전압 선택 구현 (line 1035-1057) +4. ✅ 체인 규격 선택 + 길이 입력 구현 (line 1060-1084) +5. ✅ 품목코드 자동생성 로직 변경 (line 248-305) +6. ✅ 품목 상태 이동 완료 (line 1138-1159) + +### 최종 확인사항 +- [x] 품목명 미선택 시: 품목명만 표시 +- [x] 전동개폐기 선택 시: 전원, 용량 정상 표시 +- [x] 모터 선택 시: 용량 입력, 전압 선택 정상 표시 +- [x] 체인 선택 시: 규격, 길이 정상 표시 +- [x] 품목코드 자동생성 정상 작동 +- [x] 품목 상태가 품목코드 다음에 위치 +- [x] 빌드 에러 없음 + +### 변경된 파일 +- `src/components/items/ItemForm.tsx` (6개 섹션 수정) + +### 다음 작업 대기 +모든 요구사항이 완료되었습니다. 추가 작업이 있으면 알려주세요. +``` + +### 실전 적용 예시 + +**Before (기존 방식 - 누락 발생)**: +``` +사용자: A, B, C, D 수정해줘 +AI: (A만 수정하고 B, C, D 누락) +``` + +**After (새 프로토콜 - 완벽 처리)**: +``` +사용자: A, B, C, D 수정해줘 + +AI: +[TodoWrite 생성] +1. [ ] A 수정 +2. [ ] B 수정 +3. [ ] C 수정 +4. [ ] D 수정 + +✅ 1/4 완료: A 수정 완료 (line 100-120) +✅ 2/4 완료: B 수정 완료 (line 200-250) +✅ 3/4 완료: C 수정 완료 (line 300-350) +✅ 4/4 완료: D 수정 완료 (line 400-450) + +## 전체 작업 완료 보고서 +[상세 내용...] +``` + +### 프로토콜 적용 기준 + +| 작업 복잡도 | 요구사항 수 | TodoWrite 사용 | 중간 보고 | +|------------|-----------|--------------|----------| +| 단순 (1-2개) | 1-2개 | 선택사항 | 불필요 | +| 보통 (3-5개) | 3-5개 | 필수 | 권장 | +| 복잡 (6개+) | 6개 이상 | 필수 | 필수 | + +### 예외 처리 + +**태스크 실패 시**: +```markdown +❌ 3/10 실패: 모터 필드 구현 중 에러 발생 + +**에러 내용**: +- TypeScript 타입 불일치 (line 1045) + +**해결 방안**: +1. 타입 정의 확인 +2. 수정 후 재시도 + +🔄 재시도 중... +✅ 3/10 완료: 모터 필드 구현 성공 +``` + +**의존성 문제 발견 시**: +```markdown +⚠️ 태스크 순서 변경 필요 + +**발견된 문제**: +- Task 5가 Task 3에 의존함 + +**재정렬**: +1. [x] Task 1 +2. [x] Task 2 +3. [ ] Task 3 (우선 처리) +4. [ ] Task 4 +5. [ ] Task 5 (Task 3 완료 후) +``` + +--- + +## 버전 히스토리 +- v1.0.0 (2025-01-14): 초기 버전 생성 +- 이유: ItemListClient 작업 시 text-2xl/text-3xl, font-bold 같은 미세한 차이 놓침 +- 목적: 체계적이고 완벽한 React → Next.js 마이그레이션 +- v1.1.0 (2025-01-15): Phase 4 추가 - 복잡한 다중 작업 처리 프로토콜 +- 이유: 여러 요구사항 동시 처리 시 누락 발생 방지 +- 목적: TodoWrite 기반 체계적 작업 분해 및 순차 실행 diff --git a/claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md b/claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md new file mode 100644 index 00000000..1cbdef75 --- /dev/null +++ b/claudedocs/[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md @@ -0,0 +1,662 @@ +# Zod Validation 문제 해결 가이드 + +## 문제 1: 영어 에러 메시지 표시 + +### 증상 +- 필수 필드 미입력 시 영어 에러 메시지 표시 +- 예: "Invalid input: expected string, received undefined" +- 예: "Invalid option: expected one of 'ASSEMBLY'|'BENDING'|'PURCHASED'" + +### 원인 +- `z.string()` 또는 `z.enum()`에 `undefined` 값이 들어오면 타입 체크가 먼저 실행됨 +- 커스텀 한글 에러 메시지 전에 Zod 내부 타입 에러가 먼저 발생 + +### 해결 방법: `z.preprocess()` 패턴 사용 + +#### ✅ 올바른 방법 (String 필드) +```typescript +// 상품명, 품목명 등 +const fieldSchema = z.preprocess( + (val) => val === undefined || val === null ? "" : val, + z.string().min(1, '필드명을 입력해주세요').max(200, '최대 200자') +); +``` + +#### ✅ 올바른 방법 (Enum 필드) +```typescript +// 부품 유형 등 +partType: z.preprocess( + (val) => val === undefined || val === null ? "" : val, + z.string() + .min(1, '부품 유형을 선택해주세요') + .refine( + (val) => ['ASSEMBLY', 'BENDING', 'PURCHASED'].includes(val), + { message: '부품 유형을 선택해주세요' } + ) +) +``` + +#### ❌ 잘못된 방법 +```typescript +// z.enum()은 undefined 처리 못 함 +partType: z.enum(['ASSEMBLY', 'BENDING', 'PURCHASED'], { + errorMap: () => ({ message: '부품 유형을 선택해주세요' }), +}) + +// .default()는 .min() 전에 사용 불가 +z.string().default("").min(1, 'message') // Syntax Error! +``` + +--- + +## 문제 2: 불필요한 필드 검증으로 다중 에러 발생 + +### 증상 +- 특정 품목 유형(FG, PT 등)에 없는 필드가 검증되어 에러 발생 +- 예: 제품(FG)에 가격 필드 없는데 가격 필드 검증 에러 7개 발생 + +### 원인 +- `itemMasterBaseSchema`를 모든 품목 유형이 공유 +- 특정 유형에 없는 필드도 스키마에 포함되어 검증됨 + +### 해결 방법: `.omit()` 사용 + +#### ✅ 올바른 방법 +```typescript +// 제품(FG) - 가격 정보 제거 +const productSchemaBase = itemMasterBaseSchema + .omit({ + purchasePrice: true, + salesPrice: true, + processingCost: true, + laborCost: true, + installCost: true, + }) + .merge(productFieldsSchema); +``` + +--- + +## 문제 3: 공통 필수 필드가 특정 유형에서 불필요 + +### 증상 +- `itemMasterBaseSchema`의 `itemName`이 필수인데, 부품(PT)은 `category1`을 사용 +- 부품 유형만 선택 안 해도 "품목명을 입력해주세요" 에러 발생 + +### 원인 +- `itemMasterBaseSchema`에서 `itemName: itemNameSchema` (필수) +- 부품(PT)은 `itemName` 사용 안 하고 `category1` 사용 + +### 해결 방법: `.extend()` 로 필드 오버라이드 + +#### ✅ 올바른 방법 +```typescript +// 부품(PT) - itemName을 선택 사항으로 변경 +const partSchemaBase = itemMasterBaseSchema + .extend({ + itemName: z.string().max(200).optional(), // 필수 → 선택 + }) + .merge(partFieldsSchema); +``` + +--- + +## 문제 4: 단계별 검증 (조건부 필드 검증) + +### 증상 +- 사용자 화면에 안 보이는 필드 에러가 알럿 카드에 표시됨 +- 예: 부품 유형 선택 전인데 "품목명", "설치 유형" 등 에러 동시 발생 + +### 원인 +- Zod의 `.refine()`은 모든 refinement를 순차 실행 +- 조건 체크 없이 모든 필드 검증 시도 + +### 해결 방법: `.superRefine()` + early return + +#### ✅ 올바른 방법 +```typescript +export const partSchema = partSchemaBase + .superRefine((data, ctx) => { + // 1단계: 부품 유형 필수 체크 + if (!data.partType || data.partType === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '부품 유형을 선택해주세요', + path: ['partType'], + }); + return; // 여기서 검증 중단 - 더 이상 체크 안 함 + } + + // 2단계: 부품 유형이 있을 때만 품목명 체크 + if (!data.category1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '품목명을 선택해주세요', + path: ['category1'], + }); + } + + // 3단계: 특정 부품 유형에만 해당하는 필드 + if (data.partType === 'ASSEMBLY') { + if (!data.installationType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '설치 유형을 선택해주세요', + path: ['installationType'], + }); + } + // ... 다른 필수 필드들 + } + }); +``` + +#### ❌ 잘못된 방법 +```typescript +// .refine()은 모든 체크를 실행함 +.refine((data) => !!data.partType, { ... }) +.refine((data) => !!data.category1, { ... }) // partType 없어도 실행됨! +.refine((data) => { + if (data.partType === 'ASSEMBLY') { + return !!data.installationType; // partType 없어도 실행됨! + } + return true; +}, { ... }) +``` + +--- + +## 문제 5: `.omit()` + `.extend()` + `.superRefine()` 조합 시 refinement 유실 + +### 증상 +- validation.ts에서 `superRefine()` 작성했는데 적용 안 됨 +- 여전히 단계별 검증이 작동하지 않음 +- Console.log도 나타나지 않아 superRefine 자체가 실행되지 않음 + +### 원인 +**CRITICAL**: **`.omit()`은 refinement를 제거합니다!** + +```typescript +// ❌ 잘못된 패턴 - refinement가 유실됨 +const partSchemaForForm = partSchemaBase + .omit({ createdAt: true, updatedAt: true }) + .superRefine((data, ctx) => { /* 이 부분이 실행 안 됨! */ }); + +// discriminatedUnion에서 사용 +partSchemaForForm.extend({ itemType: z.literal('PT') }) +// → Error: "Object schemas containing refinements cannot be extended" +``` + +**추가 문제**: `.extend()`도 refinement가 있는 스키마에 사용 불가 + +### 해결 방법: `.omit()` → `.merge()` → `.superRefine()` 순서 + +#### ✅ 올바른 방법 +```typescript +// 1. omit으로 불필요한 필드 제거 +// 2. merge로 itemType 추가 +// 3. superRefine을 마지막에 적용 (핵심!) +const partSchemaForForm = partSchemaBase + .omit({ createdAt: true, updatedAt: true }) + .merge(z.object({ itemType: z.literal('PT') })) + .superRefine((data, ctx) => { + // 이제 이 부분이 실행됨! + if (!data.partType || data.partType === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '부품 유형을 선택해주세요', + path: ['partType'], + }); + return; + } + + if (!data.category1 || data.category1 === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '품목명을 선택해주세요', + path: ['category1'], + }); + } + }); + +// discriminatedUnion에서는 그대로 사용 +export const createItemFormSchema = z.discriminatedUnion('itemType', [ + productSchema.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('FG') }), + partSchemaForForm, // itemType이 이미 merge되어 있음 + // ... +]); +``` + +#### ❌ 잘못된 방법들 +```typescript +// 방법 1: superRefine을 merge 전에 적용 +const wrong1 = partSchemaBase + .omit({ ... }) + .superRefine((data, ctx) => { /* 실행 안 됨 */ }) + .merge(z.object({ itemType: z.literal('PT') })); // merge가 refinement 덮어씀 + +// 방법 2: extend 사용 +const wrong2 = partSchemaBase + .omit({ ... }) + .superRefine((data, ctx) => { /* ... */ }) + .extend({ itemType: z.literal('PT') }); // Error! + +// 방법 3: discriminatedUnion에서 다시 extend +partSchemaForForm.extend({ itemType: z.literal('PT') }) // Error! +``` + +### 핵심 원칙 +1. **`.omit()`은 항상 refinement를 제거함** - 순서 상관없음 +2. **refinement는 항상 마지막에 적용** - `.merge()` 이후 +3. **`.extend()`는 refinement 있는 스키마에 사용 불가** - `.merge()` 사용 +4. **discriminatedUnion에서는 완성된 스키마 사용** - 추가 merge/extend 없이 + +--- + +## 문제 6: Form과 Validation의 필드명 불일치 + +### 증상 +- superRefine에서 early return을 사용했는데도 하위 필드 에러가 계속 나타남 +- Console.log에서 superRefine이 실행되지만, 체크하는 필드가 항상 undefined +- 예: 절곡(BENDING) 부품에서 "종류" 선택 안 해도 "재질", "폭 합계", "모양&길이" 에러 발생 + +### 원인 +**Form 컴포넌트와 Validation 스키마에서 다른 필드명을 사용** + +```typescript +// ❌ ItemForm.tsx에서 +setValue('category3', selected.code); // category3에 저장 + +// ❌ validation.ts에서 +if (!data.category2 || data.category2 === '') { // category2 체크 + // category3에 값이 있는데 category2를 체크하니까 항상 undefined! +} +``` + +### 해결 방법: 필드명 통일 + +#### ✅ 올바른 방법 +```typescript +// ItemForm.tsx - 필드명을 validation과 동일하게 +setValue('category2', selected.code); // category3 → category2로 수정 +clearErrors('category2'); + +// validation.ts - 동일한 필드명 사용 +if (!data.category2 || data.category2 === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '종류를 선택해주세요', + path: ['category2'], // 필드명 일치 + }); + return; +} +``` + +### 디버깅 방법 +1. **Form에서 setValue 호출 확인**: + - 어떤 필드명으로 값을 설정하는지 확인 + - 예: `setValue('category2', value)` 또는 `setValue('category3', value)` + +2. **Validation에서 체크하는 필드명 확인**: + - superRefine 내부에서 `data.xxx` 형태로 체크하는 필드명 확인 + - Console.log로 실제 값 확인: `console.log('category2:', data.category2, 'category3:', data.category3)` + +3. **필드명 불일치 찾기**: + ```bash + # Form 컴포넌트에서 setValue 사용 찾기 + grep -n "setValue('category" src/components/items/ItemForm.tsx + + # Validation에서 category 필드 체크 찾기 + grep -n "data.category" src/lib/utils/validation.ts + ``` + +### 예방 방법 +- **Type 정의 파일 활용**: `/src/types/item.ts`에서 필드명을 명확히 정의 +- **일관된 네이밍**: category1 (품목명), category2 (종류), category3 (하위 분류) 등 명확한 규칙 +- **코드 리뷰**: Form과 Validation 수정 시 필드명 일치 여부 확인 + +--- + +## 문제 7: Form에서 다른 곳에서 필드 값 자동 설정 + +### 증상 +- Validation에서 early return을 사용했는데도 하위 필드 에러 발생 +- Console.log에서 필드 값이 예상과 다르게 이미 설정되어 있음 +- 예: BENDING 부품에서 "종류" 선택 안 했는데 `category2: 'R'`로 이미 설정됨 + +### 원인 +**Form 컴포넌트의 다른 이벤트 핸들러에서 동일한 필드를 자동 설정** + +```typescript +// ❌ 품목명 선택 시 category2 자동 설정 (모든 부품 유형에서) +onValueChange={(val) => { + setSelectedCategory1(val); + setValue('category1', val); + const cat = PART_TYPE_CATEGORIES[selectedPartType]?.categories.find(c => c.value === val); + if (cat) setValue('category2', cat.code); // BENDING에서도 실행됨! +}} + +// validation.ts에서 +if (!data.category2 || data.category2 === '') { + // category2가 이미 'R'로 설정되어 있어서 이 체크를 통과 + return; +} +// 그래서 material 체크로 진행 → 에러 발생! +``` + +### 해결 방법: 조건부 자동 설정 + +#### ✅ 올바른 방법 +```typescript +// ItemForm.tsx - 특정 부품 유형에서만 자동 설정 +onValueChange={(val) => { + setSelectedCategory1(val); + setValue('category1', val); + const cat = PART_TYPE_CATEGORIES[selectedPartType]?.categories.find(c => c.value === val); + + // BENDING이 아닐 때만 category2 자동 설정 (BENDING은 별도로 "종류" 선택) + if (cat && selectedPartType !== 'BENDING') { + setValue('category2', cat.code); + } +}} + +// BENDING 부품의 "종류" 선택에서만 category2 설정 +onValueChange={(value) => { + setSelectedBendingItemType(value); + const selected = PART_ITEM_NAMES[selectedCategory1].find(item => item.label === value); + if (selected) { + setValue('category2', selected.code); // 여기서만 설정 + clearErrors('category2'); + } +}} +``` + +### 디버깅 방법 +1. **Console.log로 필드 값 확인**: + ```typescript + .superRefine((data, ctx) => { + console.log('🔍 검증 시작:', { + category2: data.category2, + category2Type: typeof data.category2, + }); + }) + ``` + +2. **Form 컴포넌트에서 setValue 호출 검색**: + ```bash + # 동일한 필드를 여러 곳에서 설정하는지 확인 + grep -n "setValue('category2'" src/components/items/ItemForm.tsx + ``` + +3. **예상치 못한 값 발견 시**: + - 해당 필드를 설정하는 모든 위치 확인 + - 각 위치에서 조건부 설정이 필요한지 판단 + - 부품 유형에 따라 다른 로직 적용 + +### 예방 방법 +- **명확한 필드 책임 분리**: 각 필드는 한 곳에서만 설정되도록 +- **조건부 설정 명시**: `if (partType === 'SPECIFIC')` 조건 명확히 +- **Console.log 디버깅**: 문제 발생 시 실제 값 확인 습관화 +- **필드 초기화**: 부품 유형 변경 시 관련 필드 모두 초기화 + +--- + +## 체크리스트 + +### 필수 필드 추가 시 +- [ ] `z.preprocess()` 패턴으로 undefined → "" 변환 +- [ ] `.min(1, '한글 메시지')` 사용 +- [ ] enum 타입은 `.refine()` + array.includes() 패턴 + +### 품목 유형별 스키마 작성 시 +- [ ] 해당 유형에 없는 필드는 `.omit()` 제거 +- [ ] 공통 필수 필드가 불필요하면 `.extend()` 오버라이드 +- [ ] refinement 작성 후 `createItemFormSchema`에서 사용 + +### 조건부 검증 작성 시 +- [ ] `.superRefine()` 사용 +- [ ] 필수 선행 조건 체크 후 `return`으로 중단 +- [ ] 특정 값일 때만 검증하는 필드는 `if (data.field === 'VALUE')` 체크 + +--- + +## 실전 예제: 부품(PT) 스키마 완성본 + +```typescript +// 1. 부품 전용 필드 정의 +const partFieldsSchema = z.object({ + partType: z.preprocess( + (val) => val === undefined || val === null ? "" : val, + z.string() + .min(1, '부품 유형을 선택해주세요') + .refine( + (val) => ['ASSEMBLY', 'BENDING', 'PURCHASED'].includes(val), + { message: '부품 유형을 선택해주세요' } + ) + ), + // ... 기타 선택 필드들 +}); + +// 2. Base 스키마 - itemName 제거 +const partSchemaBase = itemMasterBaseSchema + .extend({ + itemName: z.string().max(200).optional(), + }) + .merge(partFieldsSchema); + +// 3. Refinement 스키마 - 단계별 검증 +export const partSchema = partSchemaBase + .superRefine((data, ctx) => { + // 1단계: 부품 유형 필수 + if (!data.partType || data.partType === '') { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '부품 유형을 선택해주세요', + path: ['partType'], + }); + return; // 검증 중단 + } + + // 2단계: 품목명 필수 + if (!data.category1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '품목명을 선택해주세요', + path: ['category1'], + }); + } + + // 3단계: 조립 부품 전용 + if (data.partType === 'ASSEMBLY') { + if (!data.installationType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '설치 유형을 선택해주세요', + path: ['installationType'], + }); + } + // ... 기타 필수 필드 + } + + // 절곡품 전용 + if (data.partType === 'BENDING') { + // ... + } + + // 구매 부품 전용 + if (data.partType === 'PURCHASED') { + // ... + } + }); + +// 4. 폼 스키마 - .omit() + .merge() + .superRefine() 패턴 적용 +const partSchemaForForm = partSchemaBase + .omit({ createdAt: true, updatedAt: true }) + .merge(z.object({ itemType: z.literal('PT') })) + .superRefine((data, ctx) => { + // refinement 로직 (위와 동일) + }); + +export const createItemFormSchema = z.discriminatedUnion('itemType', [ + productSchema.omit({ createdAt: true, updatedAt: true }).extend({ itemType: z.literal('FG') }), + partSchemaForForm, // refinement가 마지막에 적용된 완성 스키마 + // ... +]); +``` + +--- + +## 디버깅 팁 + +### 영어 에러 메시지가 나올 때 +1. 해당 필드가 `z.preprocess()` 사용하는지 확인 +2. undefined → "" 변환 로직 있는지 확인 +3. enum 타입이면 `.refine()` 패턴으로 변경 + +### 불필요한 필드 에러가 나올 때 +1. 해당 품목 유형 스키마에서 `.omit()` 사용했는지 확인 +2. `itemMasterBaseSchema`의 필수 필드를 `.extend()` 오버라이드 했는지 확인 + +### 단계별 검증이 안 될 때 +1. `.superRefine()` 사용했는지 확인 +2. 선행 조건 체크 후 `return` 있는지 확인 +3. `createItemFormSchema`에서 refinement 포함 스키마 사용하는지 확인 +4. **CRITICAL**: `.superRefine()`이 `.merge()` **이후**에 적용되었는지 확인 +5. Console.log 추가해서 superRefine이 실행되는지 확인 +6. `.omit()` 사용했다면 반드시 refinement를 마지막에 다시 적용 +7. **CRITICAL**: **Form과 Validation의 필드명 일치** 확인! + - Form에서 `setValue('category3', value)`인데 validation에서 `data.category2` 체크하면 안 됨 + - 두 곳의 필드명이 정확히 일치해야 함 +8. **CRITICAL**: **Console.log로 실제 필드 값 확인** - 예상과 다른 값이 이미 설정되어 있는지 + - 다른 이벤트 핸들러에서 동일한 필드를 자동 설정하고 있는지 확인 + - `grep -n "setValue('필드명'" src/components/items/ItemForm.tsx`로 모든 설정 위치 확인 + +--- + +## 문제 8: 필드가 자동으로 채워져서 필수 검증이 작동하지 않음 + +### 증상 +- 부자재/원자재/소모품(SM/RM/CS) 선택 후 바로 저장 시 단위(unit) 필수 에러가 발생하지 않음 +- 에러 카드에 "품목명, 규격" 2개만 표시되고 "단위"는 누락됨 +- Zod 스키마에서는 unit을 필수로 정의했는데 검증이 안 됨 + +### 원인 +- ItemForm.tsx의 `handleItemTypeChange` 함수에서 모든 품목 유형에 대해 `setValue('unit', 'EA')` 실행 +- 부자재/원자재/소모품을 선택해도 unit 필드에 자동으로 'EA'가 설정됨 +- Zod validation에서 unit 필드가 비어있지 않다고 판단하여 필수 검증 통과 + +### 진단 방법 +```bash +# ItemForm에서 해당 필드를 설정하는 모든 위치 찾기 +grep -n "setValue('unit'" src/components/items/ItemForm.tsx +``` + +### 해결 방법 1: 조건부 초기화 + +#### ✅ 올바른 방법 +```typescript +// ItemForm.tsx - handleItemTypeChange 함수 +const handleItemTypeChange = (type: ItemType) => { + setSelectedItemType(type); + setValue('itemType', type); + + // react-hook-form 필드 초기화 + setValue('itemCode', ''); + setValue('itemName', ''); + // SM/RM/CS는 unit 필수이므로 빈 문자열로 초기화, FG/PT는 'EA' + setValue('unit', (type === 'SM' || type === 'RM' || type === 'CS') ? '' : 'EA'); + setValue('specification', ''); + // ... +}; +``` + +#### ❌ 잘못된 방법 +```typescript +// 모든 품목 유형에 동일한 기본값 설정 +setValue('unit', 'EA'); // ← SM/RM/CS도 'EA'가 들어가서 필수 검증 안 됨! +``` + +### 해결 방법 2: UI 에러 표시 추가 + +필드에 에러가 있을 때 빨간 테두리와 메시지를 표시해야 사용자가 알 수 있음 + +#### ✅ 올바른 방법 +```typescript +{/* 단위 필드 */} + +{errors.unit && ( ++ {errors.unit.message} +
+)} +``` + +### 해결 방법 3: z.object()로 완전히 새로 정의 + +`.extend()`나 `.omit()`이 제대로 작동하지 않을 때는 z.object()로 완전히 새로 정의 + +#### ✅ 올바른 방법 +```typescript +// 원자재/부자재 Base 스키마 +const materialSchemaBase = z.object({ + // 공통 필수 필드 + itemCode: z.string().optional(), + itemName: itemNameSchema, + itemType: itemTypeSchema, + specification: materialSpecificationSchema, // 필수! + unit: materialUnitSchema, // 필수! + isActive: z.boolean().default(true), + + // ... 나머지 모든 필드 명시적으로 정의 + + // 원자재/부자재 전용 필드 + material: z.string().max(100).optional(), + length: z.string().max(50).optional(), +}); +``` + +#### ❌ 잘못된 방법 +```typescript +// .extend()만으로 오버라이드 시도 (작동하지 않을 수 있음) +const materialSchemaBase = itemMasterBaseSchema + .merge(materialFieldsSchema) + .extend({ + specification: materialSpecificationSchema, // optional이 그대로 남을 수 있음 + unit: materialUnitSchema, // optional이 그대로 남을 수 있음 + }); +``` + +### 교훈 +1. **Form의 자동 설정 확인**: 필수 검증이 안 되면 Form에서 해당 필드를 자동으로 채우고 있는지 확인 +2. **조건부 초기화**: 품목 유형마다 다른 기본값이 필요하면 조건부로 설정 +3. **UI 피드백**: Validation 에러를 사용자가 볼 수 있도록 필드에 직접 표시 +4. **명시적 정의**: .extend()가 작동하지 않으면 z.object()로 완전히 새로 정의 + +--- + +## 작성일 +2025-11-15 + +## 최종 수정일 +2025-11-15 + +## 작성자 +Claude Code + +## 관련 파일 +- `/src/lib/utils/validation.ts` +- `/src/components/items/ItemForm.tsx` +- `/src/types/item.ts` \ No newline at end of file diff --git a/claudedocs/[PARTIAL-2025-11-07] auth-guard-usage.md b/claudedocs/[IMPL-2025-11-07] auth-guard-usage.md similarity index 100% rename from claudedocs/[PARTIAL-2025-11-07] auth-guard-usage.md rename to claudedocs/[IMPL-2025-11-07] auth-guard-usage.md diff --git a/claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md b/claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md index 43f578ff..a1aa73b0 100644 --- a/claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md +++ b/claudedocs/[IMPL-2025-11-12] modal-select-layout-shift-fix.md @@ -511,6 +511,618 @@ body[data-scroll-locked] { margin-right: 0 !important; } --- +## 🎯 해결한 문제 #2: 드롭다운/팝오버 위치 및 애니메이션 문제 + +### 날짜 +**2025-11-17** + +### 새로운 문제 발견 + +**문제 상황:** +- 컬러 모드 드롭다운 (DropdownMenu)과 BOM 검색 박스 (Popover)가 의도한 위치에 나타나지 않음 +- 두 가지 현상 발생: + 1. **첫 번째 시도**: 좌측에서 "날아오는" 애니메이션 효과 + 2. **두 번째 시도**: body 왼쪽 상단 (0, 0)에 고정 + +**사용자 요구사항:** +> "누른 대상의 위치를 찾고 추가된 span position 값을 absolute로 잡고 바로 누른 자리에서 나올 수 있게" + +즉, **클릭한 버튼 바로 아래에서 즉시 나타나야 함** + +--- + +### 원인 분석: 3단계 디버깅 과정 + +#### 🔍 Phase 1: 날아오는 애니메이션 원인 + +**첫 번째 시도:** +```css +/* globals.css:238-241 */ +[data-radix-popper-content-wrapper] { + will-change: auto !important; + transform: none !important; /* ← 이게 문제! */ +} +``` + +**결과:** +- ❌ 날아오는 효과는 사라졌지만... +- ❌ body 왼쪽 상단 (0, 0)에 고정되어버림! + +**왜 실패했는가:** +```typescript +// Radix UI의 위치 계산 메커니즘: +// 1. @floating-ui/react-dom이 클릭된 버튼 위치 계산 +// 2. 계산된 좌표를 transform으로 적용 +const calculatedPosition = { + x: 245, // 버튼의 x 좌표 + y: 80 // 버튼의 y 좌표 +} +element.style.transform = `translate3d(${x}px, ${y}px, 0px)` + +// ❌ 문제: transform: none !important가 이 계산을 무효화! +// 결과: element는 (0, 0)에 고정됨 +``` + +--- + +#### 🔍 Phase 2: 진짜 원인 발견 - 전역 transition + +**globals.css를 다시 분석:** +```css +/* Line 282-284: 모든 요소에 transition 적용! */ +* { + transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} +``` + +**이것이 진짜 범인이었음:** +```typescript +// Radix UI가 위치를 계산하고 적용하는 과정: + +// 1. 초기 렌더링 (Portal을 통해 body에 추가) +element.style.transform = 'translate3d(0px, 0px, 0px)' // 초기값 + +// 2. 위치 계산 완료 (Floating UI) +const position = calculatePosition(trigger, content) +// position = { x: 245, y: 80 } + +// 3. transform 업데이트 +element.style.transform = `translate3d(245px, 80px, 0px)` + +// ❌ 문제: 전역 * { transition: all } 때문에 +// transform이 즉시 변경되지 않고 +// 0,0 → 245,80으로 0.2초 동안 애니메이션됨! +// → "날아오는" 효과 발생! +``` + +**시각적 설명:** +``` +전역 transition이 없다면: +클릭 → [계산] → 즉시 (245, 80)에 나타남 ✅ + +전역 transition이 있으면: +클릭 → [계산] → (0, 0)에서 시작 → 0.2초간 이동 → (245, 80) ❌ + ↑ + "날아오는" 효과! +``` + +--- + +#### 🔍 Phase 3: 완벽한 해결책 + +**핵심 깨달음:** +1. `transform`은 **반드시 유지**해야 함 (위치 계산 필수) +2. `transition`만 **선택적으로 제거**하면 됨 +3. `animation`도 제거하면 더 깔끔 + +**최종 해결책:** +```css +/* globals.css:238-249 */ + +/* ✅ transform은 유지, transition만 제거 */ +[data-radix-popper-content-wrapper] { + will-change: auto !important; + transition: none !important; /* 핵심! 전역 transition 무효화 */ +} + +/* ✅ 추가로 slide 애니메이션도 제거 */ +[data-radix-dropdown-menu-content], +[data-radix-select-content], +[data-radix-popover-content] { + animation-name: none !important; +} +``` + +--- + +### 작동 원리 상세 분석 + +#### 1. Radix UI의 Positioning 메커니즘 + +```typescript +// Radix UI는 내부적으로 Floating UI를 사용 +import { useFloating } from '@floating-ui/react-dom' + +// 1. 트리거 요소 (버튼)의 위치 측정 +const triggerRect = trigger.getBoundingClientRect() +// { x: 245, y: 80, width: 120, height: 40 } + +// 2. 컨텐츠 요소의 크기 측정 +const contentRect = content.getBoundingClientRect() +// { width: 200, height: 150 } + +// 3. 최적 위치 계산 (충돌 방지, 뷰포트 체크) +const position = computePosition(trigger, content, { + placement: 'bottom', // 버튼 아래에 배치 + middleware: [offset(4), flip(), shift()] +}) + +// 4. 계산된 위치를 transform으로 적용 +content.style.transform = `translate3d(${position.x}px, ${position.y}px, 0px)` +``` + +#### 2. 전역 Transition의 영향 + +```css +/* globals.css에 있는 전역 스타일 */ +* { + transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} +``` + +**이 전역 transition이 미치는 영향:** +```typescript +// Before (전역 transition 있음): +element.style.transform = 'translate3d(0, 0, 0)' // 초기 +// → 0.2초 동안 transition +element.style.transform = 'translate3d(245, 80, 0)' // 최종 +// 결과: 좌측 상단에서 날아오는 효과 ❌ + +// After (transition: none 적용): +element.style.transform = 'translate3d(245, 80, 0)' // 즉시! +// 결과: 계산된 위치에 바로 나타남 ✅ +``` + +#### 3. CSS Specificity와 Override + +```css +/* 전역 스타일 (낮은 우선순위) */ +* { + transition: all 0.2s; +} +/* Specificity: 0,0,0,0 (universal selector) */ + +/* 우리의 Override (높은 우선순위) */ +[data-radix-popper-content-wrapper] { + transition: none !important; +} +/* Specificity: 0,0,1,0 + !important */ +``` + +**결과:** +- 전역 `*` 선택자보다 속성 선택자가 우선 +- `!important`로 확실히 override +- popper-content-wrapper와 그 자식들은 transition 없음 + +--- + +### 시행착오 타임라인 + +#### ❌ 시도 1: transform 제거 +```css +[data-radix-popper-content-wrapper] { + will-change: auto !important; + transform: none !important; /* 잘못된 접근 */ +} +``` +**결과:** body (0, 0)에 고정됨 + +**교훈:** Radix UI의 위치 계산에 transform이 필수임을 깨달음 + +--- + +#### ❌ 시도 2: animation만 제거 +```css +[data-radix-dropdown-menu-content], +[data-radix-select-content], +[data-radix-popover-content] { + animation-duration: 0ms !important; +} +``` +**결과:** 여전히 날아오는 효과 발생 + +**교훈:** 문제는 animation이 아니라 transition이었음 + +--- + +#### ✅ 시도 3: transition 제거 (성공!) +```css +[data-radix-popper-content-wrapper] { + will-change: auto !important; + transition: none !important; /* 핵심! */ +} +``` +**결과:** 완벽하게 작동! 클릭한 위치에서 즉시 나타남 ✅ + +**교훈:** 근본 원인을 정확히 파악하는 것이 중요 + +--- + +### 기술적 심층 분석 + +#### Floating UI의 위치 계산 알고리즘 + +```typescript +// @floating-ui/react-dom의 내부 동작 + +interface ComputePositionConfig { + placement: Placement // 'top' | 'bottom' | 'left' | 'right' ... + middleware?: Middleware[] // offset, flip, shift, arrow ... + platform?: Platform // DOM 환경 정보 +} + +function computePosition( + reference: Element, // 트리거 (버튼) + floating: Element, // 컨텐츠 (드롭다운) + config: ComputePositionConfig +): Promise
+ {formatItemCodeForAssembly(item) || '-'}
+
+` 태그 없음
+- ❌ `text-xs bg-gray-100 px-2 py-1 rounded` 배경색 스타일 없음
+
+### 품목유형 컬럼
+```tsx
+// React
+
+ {getItemTypeBadge(item.itemType)}
+ {/* + 부품인 경우 추가 뱃지 */}
+
+
+// Next.js
+
+
+ {ITEM_TYPE_LABELS[item.itemType]}
+
+
+```
+
+**차이점**:
+- ❌ `cursor-pointer` 누락
+- ❌ `getItemTypeBadge()` 함수 사용 안함 (색상 없음)
+- ❌ 부품 타입별 추가 뱃지 없음
+
+### 품목명 컬럼
+```tsx
+// React
+
+
+ {item.itemName}
+ {/* + 견적산출용 뱃지 */}
+
+
+
+// Next.js
+
+ {item.itemName}
+
+```
+
+**차이점**:
+- ❌ `cursor-pointer` 누락
+- ❌ `flex items-center gap-2` 구조 없음
+- ❌ `truncate max-w-[150px] md:max-w-none` 말줄임 없음
+- ❌ 견적산출용 뱃지 없음
+
+### 규격 컬럼
+```tsx
+// React
+
+ {item.itemCode?.includes('-') ? item.itemCode.split('-').slice(1).join('-') : (item.specification || "-")}
+
+
+// Next.js
+규격
+
+ {item.specification || '-'}
+
+```
+
+**차이점**:
+- ❌ `cursor-pointer` 누락
+- ❌ `hidden md:table-cell` 반응형 숨김 없음
+- ❌ `text-muted-foreground` → `text-gray-600` (다른 색상)
+- ❌ itemCode 파싱 로직 없음
+
+### 단위 컬럼
+```tsx
+// React
+
+ {item.unit || "-"}
+
+
+// Next.js
+단위
+{item.unit}
+```
+
+**차이점**:
+- ❌ `cursor-pointer` 누락
+- ❌ `hidden md:table-cell` 반응형 숨김 없음
+- ❌ `` 없음 (단순 텍스트)
+
+### 작업 컬럼
+```tsx
+// React
+작업
+
+ handleViewChange("view", item)}
+ onEdit={() => handleViewChange("edit", item)}
+ onDelete={() => {...}}
+ />
+
+
+// Next.js
+작업
+
+
+
+ {/* ... */}
+
+
+```
+
+**차이점**:
+- ❌ `text-right` → `text-center` (정렬 틀림)
+- ❌ `min-w-[100px]` → `w-[150px]`
+- ❌ `TableActionButtons` 컴포넌트 대신 직접 구현
+- ❌ 아이콘: `Search` → `Eye` (돋보기 → 눈)
+
+---
+
+## 📝 수정 체크리스트
+
+### 구조 변경
+- [ ] CardTitle: `text-sm md:text-base` 적용
+- [ ] TabsList 래퍼 div 추가: `overflow-x-auto -mx-2 px-2 mb-6`
+- [ ] TabsList: `inline-flex w-auto min-w-full md:grid md:w-full md:max-w-2xl md:grid-cols-6`
+- [ ] TabsTrigger: `whitespace-nowrap` 추가
+- [ ] 테이블 래퍼: `hidden lg:block rounded-md border`
+
+### 테이블 컬럼 재구성
+- [ ] 체크박스 컬럼 추가 (첫 번째, `w-[50px]`)
+- [ ] 번호 컬럼 추가 (두 번째, `hidden md:table-cell`)
+- [ ] 컬럼 순서 변경: 체크박스 → 번호 → 코드 → 유형 → 명 → 규격 → 단위 → 품목상태 → 작업
+- [ ] 판매단가 컬럼 제거 🚨
+- [ ] 상태 컬럼명 변경: "상태" → "품목 상태" ✅ (유지)
+- [ ] 작업 컬럼 정렬: `text-center` → `text-right`, width: `w-[150px]` → `min-w-[100px]`
+
+### CSS 클래스 적용
+- [ ] 품목코드: `cursor-pointer` + `` 태그 + `text-xs bg-gray-100 px-2 py-1 rounded`
+- [ ] 품목유형: `cursor-pointer` + `getItemTypeBadge()` 함수 사용
+- [ ] 품목명: `cursor-pointer` + `flex items-center gap-2` + `truncate max-w-[150px] md:max-w-none`
+- [ ] 규격: `cursor-pointer hidden md:table-cell text-muted-foreground` + itemCode 파싱 로직
+- [ ] 단위: `cursor-pointer hidden md:table-cell` + ``
+- [ ] 작업: `text-right` + `Search` 아이콘
+
+### 기능 추가
+- [ ] `getItemTypeBadge()` 함수 구현 (유형별 색상)
+- [ ] `formatItemCodeForAssembly()` 함수 구현
+- [ ] 체크박스 선택 기능
+- [ ] 견적산출용 뱃지 로직
+- [ ] 부품 타입별 추가 뱃지
+
+---
+
+## 🎯 우선순위
+
+### 긴급 (시각적 영향 큼)
+1. 번호 컬럼 추가
+2. 품목코드 배경색 (`bg-gray-100`)
+3. 품목유형 색상 (Badge)
+4. 컬럼 순서 변경
+5. 작업 정렬 수정 (`text-center` → `text-right`)
+
+### 중요
+6. 체크박스 컬럼 추가
+7. 판매단가 컬럼 제거 🚨
+8. 상태 컬럼명 변경: "상태" → "품목 상태" ✅
+9. 아이콘 변경 (Eye → Search)
+10. TabsList 반응형
+
+### 보통
+11. cursor-pointer 일괄 적용
+12. 견적산출용 뱃지
+13. 부품 타입 뱃지
\ No newline at end of file
diff --git a/claudedocs/[INDEX] DOCUMENTATION-MAP.md b/claudedocs/[INDEX] DOCUMENTATION-MAP.md
new file mode 100644
index 00000000..20bd814f
--- /dev/null
+++ b/claudedocs/[INDEX] DOCUMENTATION-MAP.md
@@ -0,0 +1,260 @@
+# 📚 프로젝트 문서 구조 및 인덱스
+
+> **프로젝트**: Next.js 15 + Laravel 하이브리드 아키텍처
+> **프론트엔드**: Next.js 15 App Router + React 19
+> **백엔드**: PHP Laravel
+> **작성일**: 2025-11-17
+> **목적**: 프로젝트 문서 아카이브 및 빠른 참조
+
+---
+
+## 📖 문서 분류 체계
+
+### 1. [GUIDE] - 개발 가이드
+프로젝트 개발 시 참고해야 할 표준 워크플로우 및 가이드 문서
+
+### 2. [IMPL-YYYY-MM-DD] - 구현 기록
+특정 기능 구현 과정과 결과를 시간순으로 기록한 문서
+
+### 3. [REF] - 참고 자료
+아키텍처 분석, 리서치 결과, API 요구사항 등 참고용 문서
+
+### 4. [PLAN] - 미래 계획
+향후 구현 예정이거나 검토 중인 기능에 대한 계획 문서
+
+### 5. [LEGACY] - 레거시 문서
+과거 설계안이나 폐기된 접근 방법을 기록한 문서
+
+---
+
+## 📂 [GUIDE] 개발 가이드 (4개)
+
+### CSS 및 마이그레이션
+| 파일명 | 목적 | 주요 내용 |
+|--------|------|-----------|
+| `[GUIDE] CSS-MIGRATION-WORKFLOW.md` | React → Next.js CSS 마이그레이션 표준 프로세스 | 페이지별 CSS 비교/동기화 워크플로우, 체크리스트 기반 구현 |
+| `[GUIDE] LARGE-FILE-WORKFLOW.md` | 대용량 파일(>1000줄) 작업 프로토콜 | 섹션별 분해 전략, 체계적 마이그레이션 방법론 |
+
+### 시스템 설계
+| 파일명 | 목적 | 주요 내용 |
+|--------|------|-----------|
+| `[GUIDE] ITEM-MANAGEMENT-MIGRATION.md` | 품목관리 시스템 마이그레이션 종합 가이드 | 하이브리드 아키텍처, 데이터 구조, API 연동 전략 |
+
+### 기술 문제 해결
+| 파일명 | 목적 | 주요 내용 |
+|--------|------|-----------|
+| `[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md` | Zod 검증 라이브러리 문제 해결 | 영어 에러 메시지 문제, z.preprocess 패턴, 필수 필드 처리 |
+
+---
+
+## 🛠️ [IMPL] 구현 기록 (25개)
+
+### 2025-11-06 (1개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-06] i18n-usage-guide.md` | 다국어(i18n) 시스템 구현 |
+
+### 2025-11-07 (7개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-07] api-key-management.md` | API 키 관리 시스템 |
+| `[IMPL-2025-11-07] auth-guard-usage.md` | 인증 가드 사용 방법 |
+| `[IMPL-2025-11-07] authentication-implementation-guide.md` | 인증 시스템 구현 가이드 |
+| `[IMPL-2025-11-07] form-validation-guide.md` | 폼 검증 시스템 |
+| `[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT 쿠키 인증 최종 구현 |
+| `[IMPL-2025-11-07] middleware-issue-resolution.md` | 미들웨어 이슈 해결 |
+| `[IMPL-2025-11-07] route-protection-architecture.md` | 라우트 보호 아키텍처 |
+| `[IMPL-2025-11-07] seo-bot-blocking-configuration.md` | SEO 봇 차단 설정 |
+
+### 2025-11-10 (2개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-10] dashboard-integration-complete.md` | 대시보드 통합 완료 |
+| `[IMPL-2025-11-10] token-management-guide.md` | 토큰 관리 시스템 |
+
+### 2025-11-11 (5개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-11] api-route-type-safety.md` | API 라우트 타입 안전성 |
+| `[IMPL-2025-11-11] chart-warning-fix.md` | 차트 경고 수정 |
+| `[IMPL-2025-11-11] dashboard-cleanup-summary.md` | 대시보드 정리 요약 |
+| `[IMPL-2025-11-11] error-pages-configuration.md` | 에러 페이지 설정 |
+| `[IMPL-2025-11-11] sidebar-active-menu-sync.md` | 사이드바 활성 메뉴 동기화 |
+
+### 2025-11-12 (1개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-12] modal-select-layout-shift-fix.md` | 모달 Select 레이아웃 시프트 수정 |
+
+### 2025-11-13 (3개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-13] browser-support-policy.md` | 브라우저 지원 정책 |
+| `[IMPL-2025-11-13] safari-cookie-compatibility.md` | Safari 쿠키 호환성 |
+| `[IMPL-2025-11-13] sidebar-scroll-improvements.md` | 사이드바 스크롤 개선 |
+
+### 2025-11-17 (1개)
+| 파일명 | 구현 내용 |
+|--------|-----------|
+| `[IMPL-2025-11-17] item-list-css-sync.md` | 품목 리스트 CSS 동기화 |
+
+---
+
+## 📋 [REF] 참고 자료 (14개)
+
+### 프로젝트 컨텍스트
+| 파일명 | 내용 |
+|--------|------|
+| `[REF] project-context.md` | 프로젝트 전체 컨텍스트 및 아키텍처 개요 |
+| `[REF] architecture-integration-risks.md` | 아키텍처 통합 리스크 분석 |
+| `[REF] code-quality-report.md` | 코드 품질 리포트 |
+| `[REF] communication_improvement_guide.md` | 커뮤니케이션 개선 가이드 |
+
+### API 및 백엔드
+| 파일명 | 내용 |
+|--------|------|
+| `[REF] api-requirements.md` | API 요구사항 (일반) |
+| `[REF] api-requirements-items.md` | 품목관리 API 요구사항 |
+| `[REF] api-analysis.md` | API 분석 |
+
+### 인증 및 보안 리서치
+| 파일명 | 내용 |
+|--------|------|
+| `[REF] nextjs15-middleware-authentication-research.md` | Next.js 15 미들웨어 인증 리서치 |
+| `[REF] token-security-nextjs15-research.md` | 토큰 보안 리서치 |
+
+### 마이그레이션 및 세션 관리
+| 파일명 | 내용 |
+|--------|------|
+| `[REF] dashboard-migration-summary.md` | 대시보드 마이그레이션 요약 |
+| `[REF] session-migration-backend.md` | 세션 마이그레이션 (백엔드) |
+| `[REF] session-migration-frontend.md` | 세션 마이그레이션 (프론트엔드) |
+| `[REF] session-migration-summary.md` | 세션 마이그레이션 요약 |
+
+### 컴포넌트 및 배포
+| 파일명 | 내용 |
+|--------|------|
+| `[REF] component-usage-analysis.md` | 컴포넌트 사용 분석 |
+| `[REF] nextjs-error-handling-guide.md` | Next.js 에러 핸들링 가이드 |
+| `[REF] production-deployment-checklist.md` | 프로덕션 배포 체크리스트 |
+
+---
+
+## 🚀 [PLAN] 미래 계획 (1개)
+
+| 파일명 | 계획 내용 |
+|--------|-----------|
+| `[PLAN] httponly-cookie-implementation.md` | HttpOnly 쿠키 구현 계획 |
+
+---
+
+## 📜 [LEGACY] 레거시 문서 (1개)
+
+| 파일명 | 내용 |
+|--------|------|
+| `[LEGACY] authentication-design.md` | 초기 인증 시스템 설계안 (폐기) |
+
+---
+
+## 🔍 빠른 검색 가이드
+
+### 상황별 문서 찾기
+
+#### 1. React → Next.js 마이그레이션 작업 시
+```
+[GUIDE] CSS-MIGRATION-WORKFLOW.md # CSS 마이그레이션 표준 프로세스
+[GUIDE] LARGE-FILE-WORKFLOW.md # 대용량 파일 작업 방법
+[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 품목관리 시스템 전체 설계
+```
+
+#### 2. 품목관리 기능 개발 시
+```
+[REF] api-requirements-items.md # 백엔드 API 요구사항
+[GUIDE] ITEM-MANAGEMENT-MIGRATION.md # 시스템 아키텍처 및 데이터 구조
+[IMPL-2025-11-17] item-list-css-sync.md # 품목 리스트 CSS 동기화 구현
+```
+
+#### 3. 인증/보안 관련 작업 시
+```
+[IMPL-2025-11-07] jwt-cookie-authentication-final.md # JWT 쿠키 인증 구현
+[IMPL-2025-11-07] route-protection-architecture.md # 라우트 보호
+[REF] token-security-nextjs15-research.md # 토큰 보안 리서치
+```
+
+#### 4. 폼 검증 문제 해결 시
+```
+[GUIDE] ZOD-VALIDATION-TROUBLESHOOTING.md # Zod 검증 문제 해결
+[IMPL-2025-11-07] form-validation-guide.md # 폼 검증 구현 가이드
+```
+
+#### 5. UI/UX 이슈 해결 시
+```
+[IMPL-2025-11-12] modal-select-layout-shift-fix.md # 모달 레이아웃 시프트
+[IMPL-2025-11-13] safari-cookie-compatibility.md # Safari 호환성
+[IMPL-2025-11-13] sidebar-scroll-improvements.md # 사이드바 스크롤
+```
+
+#### 6. 배포 준비 시
+```
+[REF] production-deployment-checklist.md # 배포 체크리스트
+[IMPL-2025-11-13] browser-support-policy.md # 브라우저 지원 정책
+[REF] code-quality-report.md # 코드 품질 리포트
+```
+
+---
+
+## 📊 문서 통계
+
+| 카테고리 | 문서 수 | 비율 |
+|----------|---------|------|
+| [GUIDE] | 4 | 8.7% |
+| [IMPL] | 25 | 54.3% |
+| [REF] | 14 | 30.4% |
+| [PLAN] | 1 | 2.2% |
+| [LEGACY] | 1 | 2.2% |
+| [INDEX] | 1 | 2.2% |
+| **합계** | **46** | **100%** |
+
+---
+
+## 🎯 문서 작성 원칙
+
+### 1. 명명 규칙
+- **[GUIDE]**: 대문자, 하이픈으로 단어 구분
+- **[IMPL-YYYY-MM-DD]**: 구현 날짜 포함, 소문자, 하이픈 구분
+- **[REF]**: 소문자, 하이픈 구분
+
+### 2. 문서 구조
+- 명확한 목차
+- 코드 예제 포함
+- 실행 가능한 명령어
+- 트러블슈팅 섹션
+
+### 3. 유지보수
+- 구현 완료 시 즉시 [IMPL] 문서 작성
+- 워크플로우 개선 시 [GUIDE] 업데이트
+- 레거시 문서는 [LEGACY]로 이동, 삭제 금지
+
+---
+
+## 📝 문서 업데이트 이력
+
+| 날짜 | 변경 내용 |
+|------|-----------|
+| 2025-11-17 | 초기 인덱스 문서 작성 |
+| 2025-11-17 | 모든 문서 명명 규칙 통일 |
+
+---
+
+## 🔗 관련 리소스
+
+- **프로젝트 루트**: `/Users/byeongcheolryu/codebridgex/sam_project/sam-next/sma-next-project/sam-react-prod`
+- **문서 디렉토리**: `claudedocs/`
+- **React 소스**: `sma-react-v2.0/`
+- **Next.js 소스**: `src/`
+
+---
+
+**마지막 업데이트**: 2025-11-17
+**문서 버전**: 1.0.0
+**관리자**: Claude + Development Team
\ No newline at end of file
diff --git a/claudedocs/00_INDEX.md b/claudedocs/[LEGACY] 00_INDEX.md
similarity index 100%
rename from claudedocs/00_INDEX.md
rename to claudedocs/[LEGACY] 00_INDEX.md
diff --git a/claudedocs/[REF-Legacy] authentication-design.md b/claudedocs/[LEGACY] authentication-design.md
similarity index 100%
rename from claudedocs/[REF-Legacy] authentication-design.md
rename to claudedocs/[LEGACY] authentication-design.md
diff --git a/claudedocs/[REF-Future] httponly-cookie-implementation.md b/claudedocs/[PLAN] httponly-cookie-implementation.md
similarity index 100%
rename from claudedocs/[REF-Future] httponly-cookie-implementation.md
rename to claudedocs/[PLAN] httponly-cookie-implementation.md
diff --git a/claudedocs/[REF] api-requirements-items.md b/claudedocs/[REF] api-requirements-items.md
new file mode 100644
index 00000000..87c582f3
--- /dev/null
+++ b/claudedocs/[REF] api-requirements-items.md
@@ -0,0 +1,918 @@
+# 품목 관리 API 요구사항 명세서
+
+**작성일**: 2025-11-17
+**최종 수정**: 2025-11-17 (v1.2)
+**대상**: PHP/Laravel 백엔드 API
+**프론트엔드**: Next.js 15 App Router
+**상태**: ✅ 프론트엔드 구현 완료, 백엔드 API 대기 중
+
+---
+
+## 📋 목차
+
+1. [리스트 화면 API (품목 목록 조회)](#1-리스트-화면-api)
+2. [품목 등록 화면 필요 데이터](#2-품목-등록-화면-필요-데이터)
+3. [품목 등록/수정 시 전송 데이터](#3-품목-등록수정-시-전송-데이터)
+
+---
+
+## 1. 리스트 화면 API
+
+### 1.1 품목 목록 조회 (GET)
+
+**엔드포인트**: `GET /api/items` 또는 `GET /api/items/paginated`
+
+**참고**:
+- `/api/items` - 전체 데이터 반환 (클라이언트 사이드 페이지네이션)
+- `/api/items/paginated` - 서버 사이드 페이지네이션 (권장)
+
+#### Request Parameters (Query String)
+
+| 파라미터 | 타입 | 필수 | 설명 | 예시 |
+|---------|------|------|------|------|
+| `itemType` | string | ❌ | 품목 유형 필터 (FG/PT/SM/RM/CS) | `FG` |
+| `search` | string | ❌ | 검색어 (품목코드, 품목명, 규격) | `스크린` |
+| `category1` | string | ❌ | 대분류 필터 | `본체부품` |
+| `category2` | string | ❌ | 중분류 필터 | `가이드시스템` |
+| `category3` | string | ❌ | 소분류 필터 | `가이드레일` |
+| `isActive` | boolean | ❌ | 활성 상태 필터 | `true` |
+| `page` | integer | ❌ | 페이지 번호 (기본값: 1) | `1` |
+| `per_page` | integer | ❌ | 페이지당 항목 수 (기본값: 50) | `50` |
+
+#### Response Body
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "id": "1",
+ "itemCode": "KD-FG-001",
+ "itemName": "스크린 제품 A",
+ "itemType": "FG",
+ "unit": "EA",
+ "specification": "2000x2000",
+ "isActive": true,
+ "category1": "본체부품",
+ "category2": "가이드시스템",
+ "category3": null,
+ "purchasePrice": 100000,
+ "salesPrice": 150000,
+ "marginRate": 33.3,
+ "processingCost": null,
+ "laborCost": null,
+ "installCost": null,
+
+ // 제품(FG) 전용 필드
+ "productName": "프리미엄 스크린",
+ "productCategory": "SCREEN",
+ "lotAbbreviation": "KD",
+ "note": null,
+
+ // 부품(PT) 전용 필드
+ "partType": null, // "ASSEMBLY" | "BENDING" | "PURCHASED"
+ "partUsage": null, // "GUIDE_RAIL" | "BOTTOM_FINISH" | "CASE" | "DOOR" | "BRACKET" | "GENERAL"
+ "installationType": null, // 조립품: "벽면형" | "측면형"
+ "assemblyType": null, // 조립품: "M" | "T" | "C" | "D" | "S" | "U"
+ "assemblyLength": null, // 조립품: "2438" | "3000" | "3500" | "4000" | "4300"
+ "material": null, // 절곡품: "EGI 1.55T" | "EGI 2.0T" | "SUS 1.2T" 등
+ "length": null, // 절곡품: 길이/목함 (mm)
+ "sideSpecWidth": null, // 조립품: 측면 규격 가로 (mm)
+ "sideSpecHeight": null, // 조립품: 측면 규격 세로 (mm)
+
+ // 버전 관리
+ "currentRevision": 0,
+ "isFinal": false,
+
+ // 메타데이터
+ "createdAt": "2025-01-10T00:00:00Z",
+ "updatedAt": null
+ }
+ ],
+ "pagination": {
+ "current_page": 1,
+ "last_page": 1,
+ "per_page": 50,
+ "total": 7
+ }
+}
+```
+
+#### 리스트 화면에서 필수로 표시되는 필드
+
+**데스크톱 테이블 컬럼 (우선순위 순)**:
+1. ✅ `id` - 체크박스 및 번호에 사용
+2. ✅ `itemCode` - 품목코드 (배경색 표시)
+3. ✅ `itemType` - 품목유형 (색상별 Badge)
+4. ✅ `partType` - 부품유형 (PT 품목에서 Badge 추가 표시)
+ - `ASSEMBLY` → "조립" (파란색 Badge)
+ - `BENDING` → "절곡" (보라색 Badge)
+ - `PURCHASED` → "구매" (녹색 Badge)
+5. ✅ `itemName` - 품목명
+6. ✅ `specification` - 규격
+7. ✅ `unit` - 단위 (Badge 표시)
+8. ✅ `isActive` - 품목 상태 (활성/비활성)
+
+**모바일 카드 레이아웃** (lg 미만):
+- 체크박스 + 품목코드 (코드 형식)
+- 품목유형 Badge + 부품유형 Badge (PT인 경우)
+- 품목명 (클릭 가능)
+- 규격 (있는 경우)
+- 단위 Badge
+- 액션 버튼 (조회/수정/삭제)
+
+**검색 및 필터링**:
+- ✅ `itemType` - 탭 및 드롭다운 필터
+- ✅ `itemCode`, `itemName`, `specification` - 통합 검색
+
+**통계 카드**:
+- ✅ 전체 품목 수
+- ✅ 품목 유형별 개수 (FG, PT, SM, RM, CS)
+
+---
+
+## 2. 품목 등록 화면 필요 데이터
+
+### 2.1 공통 마스터 데이터 조회 (GET)
+
+품목 등록 화면 진입 시 필요한 드롭다운 옵션 데이터
+
+**엔드포인트**: `GET /api/items/master-data`
+
+#### Response Body
+
+```json
+{
+ "success": true,
+ "data": {
+ // 단위 목록
+ "units": ["EA", "SET", "KG", "M", "L", "BOX", "PCS"],
+
+ // 제품 카테고리
+ "productCategories": [
+ { "code": "SCREEN", "label": "스크린" },
+ { "code": "STEEL", "label": "철재" }
+ ],
+
+ // 부품 용도
+ "partUsages": [
+ { "code": "GUIDE_RAIL", "label": "가이드레일" },
+ { "code": "BOTTOM_FINISH", "label": "하단마감재" },
+ { "code": "CASE", "label": "케이스" },
+ { "code": "DOOR", "label": "도어" },
+ { "code": "BRACKET", "label": "브라켓" },
+ { "code": "GENERAL", "label": "일반" }
+ ],
+
+ // 설치 유형
+ "installationTypes": ["벽면형", "측면형"],
+
+ // 조립품 종류
+ "assemblyTypes": ["M", "T", "C", "D", "S", "U"],
+
+ // 조립품 길이
+ "assemblyLengths": ["2438", "3000", "3500", "4000", "4300"],
+
+ // 재질 목록 (절곡품)
+ "materials": [
+ "EGI 1.55T",
+ "EGI 2.0T",
+ "SUS 1.2T",
+ "SPHC-SD 1.6T"
+ ],
+
+ // 분류 체계
+ "categories": {
+ "본체부품": {
+ "가이드시스템": ["가이드레일", "브라켓"],
+ "케이스": ["상부케이스", "하부케이스"]
+ },
+ "구조재/부속품": {
+ "볼트/너트": null,
+ "와셔": null
+ },
+ "철강재": null
+ }
+ }
+}
+```
+
+### 2.2 품목 상세 조회 (수정 모드용) (GET)
+
+**엔드포인트**: `GET /api/items/{itemCode}`
+
+**URL 파라미터**:
+- `itemCode`: 품목 코드 (예: `KD-FG-001`)
+
+#### Response Body
+
+```json
+{
+ "success": true,
+ "data": {
+ // === 공통 필드 ===
+ "id": "1",
+ "itemCode": "KD-FG-001",
+ "itemName": "스크린 제품 A",
+ "itemType": "FG",
+ "unit": "EA",
+ "specification": "2000x2000",
+ "isActive": true,
+
+ // === 분류 ===
+ "category1": "본체부품",
+ "category2": "가이드시스템",
+ "category3": null,
+
+ // === 가격 정보 ===
+ "purchasePrice": 100000,
+ "salesPrice": 150000,
+ "marginRate": 33.3,
+ "processingCost": 20000,
+ "laborCost": 15000,
+ "installCost": 10000,
+
+ // === 제품(FG) 전용 ===
+ "productName": "프리미엄 스크린",
+ "productCategory": "SCREEN",
+ "lotAbbreviation": "KD",
+ "note": "비고 내용",
+
+ // === 부품(PT) 전용 - 조립품 ===
+ "partType": "ASSEMBLY",
+ "partUsage": "GUIDE_RAIL",
+ "installationType": "벽면형",
+ "assemblyType": "M",
+ "assemblyLength": "2438",
+
+ // === 부품(PT) 전용 - 절곡품 ===
+ "bendingDiagram": "https://example.com/uploads/bending-diagram.png",
+ "bendingDetails": [
+ {
+ "id": "bd-1",
+ "no": 1,
+ "input": 100,
+ "elongation": -1,
+ "calculated": 99,
+ "sum": 99,
+ "shaded": false,
+ "aAngle": 90
+ }
+ ],
+ "material": "EGI 1.55T",
+ "length": "2000",
+
+ // === 부품(PT) 전용 - 구매품 ===
+ "electricOpenerPower": "220V",
+ "electricOpenerCapacity": "300",
+ "motorVoltage": "380V",
+
+ // === BOM (자재명세서) ===
+ "bom": [
+ {
+ "id": "bom-1",
+ "childItemCode": "KD-PT-001",
+ "childItemName": "가이드레일",
+ "quantity": 2,
+ "unit": "EA",
+ "unitPrice": 35000,
+ "quantityFormula": "H / 1000",
+ "note": "비고"
+ }
+ ],
+
+ // === 인정 정보 ===
+ "certificationNumber": "인정번호-001",
+ "certificationStartDate": "2025-01-01",
+ "certificationEndDate": "2027-12-31",
+ "specificationFile": "https://example.com/uploads/spec.pdf",
+ "specificationFileName": "시방서.pdf",
+ "certificationFile": "https://example.com/uploads/cert.pdf",
+ "certificationFileName": "인정서.pdf",
+
+ // === 메타데이터 ===
+ "safetyStock": 10,
+ "leadTime": 7,
+ "currentRevision": 0,
+ "isFinal": false,
+ "createdAt": "2025-01-10T00:00:00Z",
+ "updatedAt": "2025-01-12T00:00:00Z"
+ }
+}
+```
+
+### 2.3 BOM 품목 검색 (GET)
+
+BOM 추가 시 하위 품목 검색용 - **2개의 분리된 API**로 구현
+
+#### 2.3.1 품목 코드 검색 (자동완성)
+
+**엔드포인트**: `GET /api/items/search/codes`
+
+**Query Parameters**:
+- `q`: 검색어 (품목코드)
+- `limit`: 결과 개수 제한 (기본값: 10)
+
+**Response Body**:
+```json
+{
+ "success": true,
+ "data": ["KD-PT-001", "KD-PT-002", "KD-PT-003"]
+}
+```
+
+#### 2.3.2 품목명 검색 (자동완성)
+
+**엔드포인트**: `GET /api/items/search/names`
+
+**Query Parameters**:
+- `q`: 검색어 (품목명)
+- `limit`: 결과 개수 제한 (기본값: 10)
+
+**Response Body**:
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "itemCode": "KD-PT-001",
+ "itemName": "가이드레일"
+ },
+ {
+ "itemCode": "KD-PT-002",
+ "itemName": "가이드레일 브라켓"
+ }
+ ]
+}
+```
+
+#### 2.3.3 통합 품목 검색 (BOM 추가용)
+
+**엔드포인트**: `GET /api/items/search`
+
+**Query Parameters**:
+- `q`: 검색어 (품목코드 또는 품목명)
+- `itemType`: 품목 유형 필터 (선택)
+- `limit`: 결과 개수 제한 (기본값: 10)
+
+**Response Body**:
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "itemCode": "KD-PT-001",
+ "itemName": "가이드레일",
+ "itemType": "PT",
+ "partType": "BENDING",
+ "unit": "EA",
+ "specification": "2438mm",
+ "purchasePrice": 35000,
+ "salesPrice": 50000
+ }
+ ]
+}
+```
+
+---
+
+## 3. 품목 등록/수정 시 전송 데이터
+
+### 3.1 품목 등록 (POST)
+
+**엔드포인트**: `POST /api/items`
+
+**Content-Type**: `multipart/form-data` (파일 업로드 포함 시)
+
+#### Request Body
+
+```json
+{
+ // === 공통 필드 (모든 품목 유형) ===
+ "itemCode": "KD-FG-001", // 자동생성 또는 수동입력
+ "itemName": "스크린 제품 A",
+ "itemType": "FG", // FG/PT/SM/RM/CS
+ "unit": "EA",
+ "specification": "2000x2000",
+ "isActive": true,
+
+ // === 분류 ===
+ "category1": "본체부품",
+ "category2": "가이드시스템",
+ "category3": null,
+
+ // === 가격 정보 ===
+ "purchasePrice": 100000,
+ "salesPrice": 150000,
+ "marginRate": 33.3, // 자동계산 또는 수동입력
+ "processingCost": 20000,
+ "laborCost": 15000,
+ "installCost": 10000,
+
+ // === 제품(FG) 전용 필드 ===
+ "productName": "프리미엄 스크린",
+ "productCategory": "SCREEN",
+ "lotAbbreviation": "KD",
+ "note": "비고 내용",
+
+ // === 부품(PT) 전용 필드 - 조립품 ===
+ "partType": "ASSEMBLY", // ASSEMBLY/BENDING/PURCHASED
+ "partUsage": "GUIDE_RAIL",
+ "installationType": "벽면형",
+ "assemblyType": "M",
+ "assemblyLength": "2438",
+
+ // === 부품(PT) 전용 필드 - 절곡품 ===
+ "material": "EGI 1.55T",
+ "length": "2000",
+ "bendingLength": "2000",
+ "bendingDetails": [
+ {
+ "no": 1,
+ "input": 100,
+ "elongation": -1,
+ "calculated": 99,
+ "sum": 99,
+ "shaded": false,
+ "aAngle": 90
+ }
+ ],
+
+ // === 부품(PT) 전용 필드 - 구매품 ===
+ "electricOpenerPower": "220V",
+ "electricOpenerCapacity": "300",
+ "motorVoltage": "380V",
+ "motorCapacity": "500",
+ "chainSpec": "체인규격",
+
+ // === BOM (자재명세서) ===
+ "bom": [
+ {
+ "childItemCode": "KD-PT-001",
+ "childItemName": "가이드레일",
+ "quantity": 2,
+ "unit": "EA",
+ "unitPrice": 35000,
+ "quantityFormula": "H / 1000", // 수량 계산식 (선택)
+ "note": "비고",
+
+ // 절곡품 BOM인 경우
+ "isBending": true,
+ "width": 100,
+ "bendingDetails": [...]
+ }
+ ],
+
+ // === 인정 정보 (제품/부품) ===
+ "certificationNumber": "인정번호-001",
+ "certificationStartDate": "2025-01-01",
+ "certificationEndDate": "2027-12-31",
+
+ // === 메타데이터 ===
+ "safetyStock": 10,
+ "leadTime": 7,
+ "isVariableSize": false,
+ "currentRevision": 0,
+ "isFinal": false
+}
+```
+
+#### 파일 업로드 (FormData)
+
+```javascript
+const formData = new FormData();
+
+// JSON 데이터
+formData.append('data', JSON.stringify(itemData));
+
+// 파일들
+formData.append('specificationFile', specificationFile); // 시방서
+formData.append('certificationFile', certificationFile); // 인정서
+formData.append('bendingDiagram', bendingDiagramFile); // 절곡품 전개도
+```
+
+#### Response Body (성공)
+
+```json
+{
+ "success": true,
+ "message": "품목이 등록되었습니다.",
+ "data": {
+ "id": "1",
+ "itemCode": "KD-FG-001",
+ // ... 전체 품목 데이터
+ }
+}
+```
+
+#### Response Body (실패)
+
+```json
+{
+ "success": false,
+ "message": "품목 등록에 실패했습니다.",
+ "errors": {
+ "itemName": ["품목명은 필수입니다."],
+ "unit": ["단위는 필수입니다."]
+ }
+}
+```
+
+### 3.2 품목 수정 (PUT)
+
+**엔드포인트**: `PUT /api/items/{itemCode}`
+
+**Request Body**: 품목 등록과 동일 (변경된 필드만 전송 가능)
+
+**참고**:
+- `itemType`은 수정 불가 (품목 유형 변경 시 신규 등록 필요)
+- 파일은 새로운 파일 업로드 시만 전송
+
+### 3.3 품목 삭제 (DELETE)
+
+**엔드포인트**: `DELETE /api/items/{itemCode}`
+
+#### Response Body
+
+```json
+{
+ "success": true,
+ "message": "품목이 삭제되었습니다."
+}
+```
+
+---
+
+## 4. 데이터 검증 규칙
+
+### 4.1 공통 필수 필드
+
+모든 품목 유형에서 필수:
+- ✅ `itemType` - 품목 유형
+- ✅ `itemName` - 품목명
+- ✅ `unit` - 단위
+- ✅ `isActive` - 활성 상태 (기본값: true)
+
+### 4.2 제품(FG) 필수 필드
+
+- ✅ `productName` - 상품명
+- ✅ `itemName` - 품목명
+- ✅ `itemCode` - 자동생성: `{productName}-{itemName}`
+
+### 4.3 부품(PT) 필수 필드
+
+**조립품 (ASSEMBLY)**:
+- ✅ `itemName` - 품목명
+- ✅ `length` - 길이
+- ✅ `itemCode` - 자동생성 규칙 있음
+
+**절곡품 (BENDING)**:
+- ✅ `itemName` - 품목명
+- ✅ `length` - 길이/목함
+- ✅ `specification` - 규격 (재질)
+- ✅ `itemCode` - 자동생성 규칙 있음
+
+**구매품 (PURCHASED)**:
+- ✅ `itemName` - 품목명
+- ✅ `specification` - 규격
+- ✅ `itemCode` - 자동생성 규칙 있음
+
+### 4.4 부자재/원자재/소모품 (SM/RM/CS) 필수 필드
+
+- ✅ `itemName` - 품목명
+- ✅ `unit` - 단위
+- ✅ `specification` - 규격
+- ✅ `itemCode` - 자동생성 규칙 있음
+
+---
+
+## 5. 품목 코드 자동생성 규칙
+
+### 5.1 제품 (FG)
+
+**형식**: `{상품명}-{품목명}`
+
+**예시**:
+- 상품명: `프리미엄 스크린`
+- 품목명: `2000x2000`
+- 결과: `프리미엄 스크린-2000x2000`
+
+### 5.2 부품 (PT) - 조립품
+
+**형식**: `KD-{설치유형코드}{조립종류}{길이}`
+
+**예시**:
+- 설치유형: 벽면형 → `M`
+- 조립종류: `T`
+- 길이: `2438`
+- 결과: `KD-MT2438`
+
+### 5.3 부품 (PT) - 절곡품
+
+**형식**: `{재질}-{길이/목함}`
+
+**예시**:
+- 재질: `EGI 1.55T`
+- 길이: `2000`
+- 결과: `EGI 1.55T-2000`
+
+### 5.4 부품 (PT) - 구매품
+
+**형식**: `{품목명}`
+
+**예시**: `전동개폐기 220V 300KG`
+
+### 5.5 부자재/원자재/소모품 (SM/RM/CS)
+
+**형식**: 수동 입력 또는 `{품목명}-{규격}`
+
+**예시**:
+- 품목명: `볼트`
+- 규격: `M6x20`
+- 결과: `볼트-M6x20`
+
+---
+
+## 6. 파일 업로드 요구사항
+
+> **참조**: `/downloads/file_storage_implementation_guide.md` - 파일 저장소 시스템 전체 구현 가이드
+
+### 6.1 허용 파일 형식
+
+**기본 정책**:
+- **최대 파일 크기**: 20MB
+- **파일명 처리**:
+ - 사용자가 보는 이름 (display_name): 원본 파일명 유지
+ - 실제 저장 이름 (stored_name): 64bit 난수 (16자 hex) + 확장자
+
+| 파일 종류 | 허용 확장자 | MIME 타입 | 비고 |
+|----------|-----------|----------|------|
+| **시방서** | `.pdf`, `.docx`, `.hwp`, `.jpg`, `.png` | `application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/x-hwp`, `image/jpeg`, `image/png` | 문서 및 이미지 형식 모두 지원 |
+| **인정서** | `.pdf`, `.docx`, `.hwp`, `.jpg`, `.png` | `application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/x-hwp`, `image/jpeg`, `image/png` | 문서 및 이미지 형식 모두 지원 |
+| **절곡품 전개도** | `.jpg`, `.png`, `.pdf` | `image/jpeg`, `image/png`, `application/pdf` | 이미지 및 PDF 형식 |
+| **기타 첨부** | `.xlsx`, `.xls`, `.csv`, `.zip`, `.rar` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `application/vnd.ms-excel`, `text/csv`, `application/zip`, `application/x-rar-compressed` | Excel, 압축 파일 등 |
+
+**차단 확장자** (보안):
+```
+exe, sh, bat, cmd, dwg, dxf, step, iges
+```
+
+### 6.2 파일 저장 경로
+
+**경로 구조** (테넌트별 분리):
+```
+storage/app/tenants/{tenant_id}/{folder_key}/{year}/{month}/{stored_name}
+```
+
+**품목 관련 파일 경로 예시**:
+```
+storage/app/tenants/1/product/2025/01/a1b2c3d4e5f6g7h8.pdf
+storage/app/tenants/1/product/2025/01/i9j0k1l2m3n4o5p6.jpg
+```
+
+**임시 업로드 경로** (temp 폴더):
+```
+storage/app/tenants/{tenant_id}/temp/{year}/{month}/{stored_name}
+```
+
+### 6.3 파일 업로드 프로세스
+
+```
+[Frontend] 파일 선택 → multipart/form-data 전송
+ ↓
+[Backend] 파일 검증
+ - 확장자 체크 (허용 목록)
+ - MIME 타입 검증
+ - 파일 크기 체크 (20MB 이하)
+ - 용량 체크 (테넌트 용량 확인)
+ ↓
+[Backend] temp 폴더에 임시 저장
+ - 난수 파일명 생성 (16자 hex + 확장자)
+ - 경로: /tenants/{id}/temp/{year}/{month}/{random}.{ext}
+ - DB 저장 (is_temp=true, folder_id=NULL)
+ ↓
+[Response] { file_id, display_name, file_size, mime_type }
+ ↓
+[Frontend] 품목 등록 시 file_id 전송
+ ↓
+[Backend] 문서 저장 후 파일 이동
+ - temp → product 폴더로 이동
+ - DB 업데이트 (is_temp=false, folder_id, document_id)
+```
+
+### 6.4 파일 응답 형식
+
+**업로드 성공 응답**:
+```json
+{
+ "success": true,
+ "data": {
+ "file_id": 123,
+ "display_name": "시방서.pdf",
+ "stored_name": "a1b2c3d4e5f6g7h8.pdf",
+ "file_size": 1024000,
+ "mime_type": "application/pdf",
+ "file_type": "document",
+ "is_temp": true,
+ "created_at": "2025-01-17T10:00:00Z"
+ }
+}
+```
+
+**파일 URL 형식**:
+```
+GET /api/files/{file_id}/download
+→ 파일 스트리밍 응답 (Content-Disposition: attachment)
+```
+
+### 6.5 에러 응답
+
+| HTTP 코드 | 에러 상황 | 메시지 예시 |
+|----------|----------|-----------|
+| 400 | 파일 없음 | `No file uploaded` |
+| 400 | 차단된 확장자 | `File extension '.exe' is not allowed` |
+| 400 | MIME 타입 불일치 | `Invalid MIME type` |
+| 413 | 파일 크기 초과 | `File size exceeds 20MB limit` |
+| 413 | 용량 초과 | `Storage quota exceeded. Please delete files or contact support.` |
+| 422 | 처리 불가 | `Failed to store file` |
+
+---
+
+## 7. 에러 코드
+
+| HTTP 코드 | 설명 | 예시 |
+|----------|------|------|
+| 200 | 성공 | 조회, 수정, 삭제 성공 |
+| 201 | 생성 성공 | 품목 등록 성공 |
+| 400 | 잘못된 요청 | 필수 필드 누락, 유효성 검증 실패 |
+| 404 | 리소스 없음 | 품목을 찾을 수 없음 |
+| 409 | 충돌 | 품목코드 중복 |
+| 422 | 처리 불가 | 비즈니스 로직 오류 |
+| 500 | 서버 오류 | 예상치 못한 서버 오류 |
+
+---
+
+## 8. 다음 단계
+
+### 8.1 우선순위 1: 리스트 화면 API
+- [ ] `GET /api/items` 구현
+- [ ] 페이지네이션 구현
+- [ ] 검색 및 필터링 구현
+
+### 8.2 우선순위 2: 마스터 데이터 API
+- [ ] `GET /api/items/master-data` 구현
+- [ ] 드롭다운 옵션 데이터 제공
+
+### 8.3 우선순위 3: 품목 등록 API
+- [ ] `POST /api/items` 구현
+- [ ] 파일 업로드 처리
+- [ ] 품목코드 자동생성 로직
+
+### 8.4 우선순위 4: 품목 수정/삭제 API
+- [ ] `GET /api/items/{itemCode}` 구현
+- [ ] `PUT /api/items/{itemCode}` 구현
+- [ ] `DELETE /api/items/{itemCode}` 구현
+
+### 8.5 우선순위 5: BOM 검색 API
+- [ ] `GET /api/items/search` 구현
+
+---
+
+## 9. 프론트엔드 구현 현황 (2025-11-17)
+
+### ✅ 완료된 화면
+
+#### 품목 목록 화면
+- **경로**: `/[locale]/(protected)/items`
+- **컴포넌트**: `ItemListClient.tsx`
+- **기능**:
+ - 품목 유형별 탭 필터 (전체/제품/부품/부자재/원자재/소모품)
+ - 통합 검색 (품목코드, 품목명, 규격)
+ - 데스크톱 테이블 + 모바일 카드 반응형 레이아웃
+ - 페이지네이션 (클라이언트 사이드)
+ - 일괄 삭제
+ - 품목유형 + 부품유형 Badge 표시
+
+#### 품목 상세 화면
+- **경로**: `/[locale]/(protected)/items/[itemCode]`
+- **컴포넌트**: `ItemDetailClient.tsx`
+- **기능**:
+ - 품목 유형별 조건부 섹션 표시
+ - 제품(FG): 기본 정보, 제품 정보, BOM
+ - 부품(PT) - 조립: 기본 정보, 조립 부품 세부 정보, BOM
+ - 부품(PT) - 절곡: 기본 정보, 가이드레일 세부 정보
+ - 부품(PT) - 구매: 기본 정보
+ - 부자재/원자재/소모품: 기본 정보
+ - BOM 테이블 표시
+
+#### 품목 등록/수정 화면
+- **경로**:
+ - 등록: `/[locale]/(protected)/items/new`
+ - 수정: `/[locale]/(protected)/items/[itemCode]/edit`
+- **상태**: 🚧 개발 예정
+
+### ✅ 구현된 타입 정의
+- **파일**: `src/types/item.ts`
+- **타입**: `ItemMaster`, `BOMLine`, `BendingDetail`, `ItemType`, `PartType` 등 완료
+
+### ✅ 구현된 API 클라이언트
+- **파일**: `src/lib/api/items.ts`
+- **함수**:
+ - `fetchItems()` - 목록 조회
+ - `fetchItemsPaginated()` - 페이지네이션 목록
+ - `fetchItemByCode()` - 상세 조회
+ - `createItem()` - 등록
+ - `updateItem()` - 수정
+ - `deleteItem()` - 삭제
+ - `uploadFile()` - 파일 업로드
+ - `searchItemCodes()` - 코드 검색
+ - `searchItemNames()` - 품목명 검색
+
+### 🚧 개발 대기 중
+- 품목 등록/수정 폼 화면
+- BOM 관리 인터페이스
+- 절곡품 전개도 편집기
+
+---
+
+## 10. 백엔드 API 구현 우선순위
+
+### Phase 1: 필수 API (회의 직후 착수)
+1. ✅ `GET /api/items` - 품목 목록 조회
+2. ✅ `GET /api/items/{itemCode}` - 품목 상세 조회
+3. ✅ `GET /api/items/master-data` - 마스터 데이터 조회
+
+### Phase 2: CRUD API
+4. ✅ `POST /api/items` - 품목 등록
+5. ✅ `PUT /api/items/{itemCode}` - 품목 수정
+6. ✅ `DELETE /api/items/{itemCode}` - 품목 삭제
+
+### Phase 3: 검색 및 유틸리티
+7. ✅ `GET /api/items/search` - 통합 검색
+8. ✅ `GET /api/items/search/codes` - 코드 검색
+9. ✅ `GET /api/items/search/names` - 품목명 검색
+10. ✅ `POST /api/items/{itemCode}/files` - 파일 업로드
+
+### Phase 4: BOM 관리
+11. ✅ `GET /api/items/{itemCode}/bom` - BOM 조회
+12. ✅ `POST /api/items/{itemCode}/bom` - BOM 라인 추가
+13. ✅ `PUT /api/items/{itemCode}/bom/{lineId}` - BOM 라인 수정
+14. ✅ `DELETE /api/items/{itemCode}/bom/{lineId}` - BOM 라인 삭제
+
+---
+
+## 11. 회의 안건 (PHP 백엔드 팀)
+
+### 1. API 엔드포인트 확정
+- `/api/items` vs `/api/items/paginated` 중 선택
+- 검색 API 분리 방식 (codes, names) 승인
+
+### 2. 데이터베이스 스키마 검토
+- `items` 테이블 구조
+- `bom_lines` 테이블 구조
+- `item_revisions` 테이블 (버전 관리)
+- 파일 저장 경로 및 구조
+
+### 3. 인증 방식 확인
+- Bearer Token vs Cookie 방식
+- CORS 설정
+
+### 4. 파일 업로드 구현
+- **참조 문서**: `/downloads/file_storage_implementation_guide.md`
+- 저장 경로: `storage/app/tenants/{tenant_id}/{folder_key}/{year}/{month}/{stored_name}`
+- 최대 파일 크기: 20MB
+- 허용 확장자:
+ - 문서: pdf, docx, hwp
+ - 이미지: jpg, png
+ - 기타: xlsx, xls, csv, zip, rar
+- 차단 확장자: exe, sh, bat, cmd, dwg, dxf, step, iges
+- 파일명 처리: 난수 저장명 (16자 hex) + 원본명 보존 (display_name)
+
+### 5. 에러 응답 형식 통일
+```json
+{
+ "success": false,
+ "message": "에러 메시지",
+ "errors": {
+ "fieldName": ["검증 실패 메시지"]
+ }
+}
+```
+
+### 6. 개발 일정 협의
+- Phase 1 (필수 API): 목표 일정
+- Phase 2-4: 순차 개발 일정
+
+---
+
+## 12. 버전 히스토리
+
+- **v1.0** (2025-11-17 09:00): 초안 작성, API 요구사항 정의
+- **v1.1** (2025-11-17 17:30): 프론트엔드 구현 현황 반영, 검색 API 세분화, 모바일 레이아웃 추가, 회의 안건 작성
+- **v1.2** (2025-11-17 회의 후): 파일 업로드 요구사항 개정 (회의 결과 반영)
+ - 시방서/인정서: PDF뿐만 아니라 이미지(JPG, PNG), 문서(DOCX, HWP) 형식 지원
+ - 최대 파일 크기: 10MB → 20MB로 증가
+ - 파일 저장소 구현 가이드 참조 추가 (`/downloads/file_storage_implementation_guide.md`)
+ - 테넌트별 파일 저장 경로 구조 명시
+ - 파일명 처리 방식 명시 (난수 저장명 + 원본명 보존)
+ - 차단 확장자 목록 추가 (보안)
\ No newline at end of file
diff --git a/claudedocs/[REF-2025-11-12] component-usage-analysis.md b/claudedocs/[REF] component-usage-analysis.md
similarity index 100%
rename from claudedocs/[REF-2025-11-12] component-usage-analysis.md
rename to claudedocs/[REF] component-usage-analysis.md
diff --git a/claudedocs/[REF-2025-11-10] dashboard-migration-summary.md b/claudedocs/[REF] dashboard-migration-summary.md
similarity index 100%
rename from claudedocs/[REF-2025-11-10] dashboard-migration-summary.md
rename to claudedocs/[REF] dashboard-migration-summary.md
diff --git a/claudedocs/[REF-2025-11-07] research_nextjs15_middleware_authentication.md b/claudedocs/[REF] nextjs15-middleware-authentication-research.md
similarity index 100%
rename from claudedocs/[REF-2025-11-07] research_nextjs15_middleware_authentication.md
rename to claudedocs/[REF] nextjs15-middleware-authentication-research.md
diff --git a/claudedocs/[REF-2025-11-12] session-migration-backend.md b/claudedocs/[REF] session-migration-backend.md
similarity index 100%
rename from claudedocs/[REF-2025-11-12] session-migration-backend.md
rename to claudedocs/[REF] session-migration-backend.md
diff --git a/claudedocs/[REF-2025-11-12] session-migration-frontend.md b/claudedocs/[REF] session-migration-frontend.md
similarity index 100%
rename from claudedocs/[REF-2025-11-12] session-migration-frontend.md
rename to claudedocs/[REF] session-migration-frontend.md
diff --git a/claudedocs/[REF-2025-11-12] session-migration-summary.md b/claudedocs/[REF] session-migration-summary.md
similarity index 100%
rename from claudedocs/[REF-2025-11-12] session-migration-summary.md
rename to claudedocs/[REF] session-migration-summary.md
diff --git a/claudedocs/[REF-2025-11-07] research_token_security_nextjs15.md b/claudedocs/[REF] token-security-nextjs15-research.md
similarity index 100%
rename from claudedocs/[REF-2025-11-07] research_token_security_nextjs15.md
rename to claudedocs/[REF] token-security-nextjs15-research.md
diff --git a/claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md b/claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md
new file mode 100644
index 00000000..c5129a87
--- /dev/null
+++ b/claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md
@@ -0,0 +1,164 @@
+# [SESSION-2025-11-18] localStorage SSR 수정 작업 체크포인트
+
+## 세션 상태: 진행 중 (0/6 완료)
+
+### 작업 개요
+- **목표**: ItemMasterDataManagement.tsx의 모든 localStorage 접근을 SSR 호환으로 수정
+- **파일**: `src/components/items/ItemMasterDataManagement.tsx`
+- **크기**: 274KB (대용량 파일)
+- **진행률**: 0/6 완료
+
+### 작업 배경
+- React → Next.js 마이그레이션 작업 진행 중
+- SSR 환경에서 localStorage 접근 시 `ReferenceError: localStorage is not defined` 에러 발생
+- `typeof window === 'undefined'` 체크를 통한 SSR 호환성 확보 필요
+
+### 수정 대상 (6곳)
+
+#### 1. attributeSubTabs (Line ~460)
+```typescript
+// 현재 코드
+const [attributeSubTabs, setAttributeSubTabs] = useState>(() => {
+ const saved = localStorage.getItem('mes-attributeSubTabs'); // ❌ SSR 오류
+ // ...
+});
+
+// 수정 필요
+const [attributeSubTabs, setAttributeSubTabs] = useState>(() => {
+ if (typeof window === 'undefined') {
+ return [
+ { id: 'units', label: '단위', key: 'units', isDefault: true, order: 0 },
+ { id: 'materials', label: '재질', key: 'materials', isDefault: true, order: 1 },
+ { id: 'surface', label: '표면처리', key: 'surface', isDefault: true, order: 2 }
+ ];
+ }
+ const saved = localStorage.getItem('mes-attributeSubTabs');
+ // ...
+});
+```
+**상태**: ❌ 미완료
+
+#### 2. attributeColumns (Line ~668)
+```typescript
+// 현재 코드
+const [attributeColumns, setAttributeColumns] = useState>(() => {
+ const saved = localStorage.getItem('attribute-columns'); // ❌ SSR 오류
+ return saved ? JSON.parse(saved) : {};
+});
+
+// 수정 필요
+const [attributeColumns, setAttributeColumns] = useState>(() => {
+ if (typeof window === 'undefined') return {};
+ const saved = localStorage.getItem('attribute-columns');
+ return saved ? JSON.parse(saved) : {};
+});
+```
+**상태**: ❌ 미완료
+
+#### 3. bomItems (Line ~820)
+```typescript
+// 현재 코드
+const [bomItems, setBomItems] = useState(() => {
+ const saved = localStorage.getItem('bom-items'); // ❌ SSR 오류
+ return saved ? JSON.parse(saved) : [];
+});
+
+// 수정 필요
+const [bomItems, setBomItems] = useState(() => {
+ if (typeof window === 'undefined') return [];
+ const saved = localStorage.getItem('bom-items');
+ return saved ? JSON.parse(saved) : [];
+});
+```
+**상태**: ❌ 미완료
+
+#### 4-6. 추가 localStorage 사용 위치 (검색 필요)
+**검색 명령**:
+```bash
+grep -n "localStorage.getItem\|localStorage.setItem" src/components/items/ItemMasterDataManagement.tsx
+```
+**상태**: ❌ 확인 필요
+
+### 작업 계획
+
+#### Phase 1: 전체 localStorage 사용 위치 파악
+```bash
+grep -n "localStorage" src/components/items/ItemMasterDataManagement.tsx > /tmp/localstorage-usage.txt
+```
+
+#### Phase 2: useState 초기화 수정
+- attributeSubTabs 수정
+- attributeColumns 수정
+- bomItems 수정
+- 기타 발견된 useState 초기화 수정
+
+#### Phase 3: useEffect 내부 수정 (필요 시)
+- useEffect 내부의 localStorage 접근은 SSR 안전 (클라이언트에서만 실행)
+- 필요 시 체크 추가
+
+#### Phase 4: 테스트 및 검증
+```bash
+# 빌드 테스트
+npm run build
+
+# 타입 체크
+npm run type-check
+
+# 개발 서버 실행
+npm run dev
+```
+
+### 세션 재개 방법
+
+#### 다음 세션 시작 시
+```bash
+# 1. 이 문서 확인
+cat claudedocs/[SESSION-2025-11-18] localStorage-ssr-fix-checkpoint.md
+
+# 2. 작업 재개
+"localStorage SSR 수정 작업 이어서 진행해줘"
+```
+
+#### 또는 /sc:load 사용
+```bash
+/sc:load
+# 자동으로 이 체크포인트를 로드하여 작업 재개
+```
+
+### 주의사항
+
+#### 대용량 파일 작업 전략
+- ✅ **섹션별 작업**: 한 번에 1-2개 수정, 즉시 커밋
+- ✅ **빈번한 커밋**: 5분마다 WIP 커밋
+- ✅ **토큰 관리**: 불필요한 파일 Read 최소화
+- ❌ **한 번에 전체 수정 금지**: 세션 중단 위험
+
+#### 세션 중단 방지
+```yaml
+checkpoint_strategy:
+ interval: "5-10분마다 커밋"
+ pattern: "수정 → 커밋 → 수정 → 커밋"
+ max_continuous_work: "15분"
+```
+
+### 관련 문서
+- `[GUIDE] LARGE-FILE-WORKFLOW.md` - 대용량 파일 작업 가이드
+- `[REF] nextjs15-middleware-authentication-research.md` - SSR 호환성 참고
+
+### 체크리스트
+
+- [ ] Phase 1: localStorage 사용 위치 전체 파악
+- [ ] Phase 2-1: attributeSubTabs 수정
+- [ ] Phase 2-2: attributeColumns 수정
+- [ ] Phase 2-3: bomItems 수정
+- [ ] Phase 2-4: 추가 useState 초기화 수정
+- [ ] Phase 3: useEffect 내부 체크 (필요 시)
+- [ ] Phase 4-1: 빌드 테스트
+- [ ] Phase 4-2: 타입 체크
+- [ ] Phase 4-3: 개발 서버 테스트
+- [ ] 최종 커밋 및 문서 업데이트
+
+---
+
+**세션 저장 시간**: 2025-11-18
+**다음 작업**: Phase 1부터 재개
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
new file mode 100644
index 00000000..e69de29b