- claudedocs 폴더 구조 재정리: archive/sessions, guides/migration·mobile·universal-list, refactoring 분류 - 오래된 세션 컨텍스트/체크리스트 문서 정리 (아카이브 이동 또는 삭제) - AuthContext → authStore(Zustand) 전환 시작, RootProvider 간소화 - GenericCRUDDialog 공통 다이얼로그 컴포넌트 추가 - PermissionDialog 삭제 → GenericCRUDDialog로 대체 - RankDialog/TitleDialog GenericCRUDDialog 기반으로 리팩토링 - toast-utils.ts 삭제 (미사용) - fileDownload.ts 개선, excel-download.ts 정리 - menuStore/themeStore Zustand 셀렉터 최적화 - useColumnSettings/useTableColumnStore 기능 보강 - 세금계산서/견적/작업자화면/결재 등 소규모 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
17 KiB
프로젝트 기술 결정 사항
_index.md에서 분리됨 (2026-02-23). 프로젝트 전반의 기술 선택 배경과 근거를 기록.
<img> 태그 사용 — next/image 미사용 이유 (2026-02-10)
현황: 프로젝트 전체 <img> 태그 10건, next/image 0건
결정: <img> 유지, next/image 전환 불필요
근거:
- 폐쇄형 ERP 시스템 — SEO 불필요, LCP 점수 무의미
- 전량 외부 동적 이미지 — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건)
- 프린트/문서 레이아웃 — 10건 중 8건이 검사 기준서·도해 등 인쇄용.
next/image의width/height강제 지정이 프린트 레이아웃을 깰 위험 - blob URL 비호환 — 업로드 미리보기(blob:)는
next/image가 지원 안 함 - 설정 부담 > 이점 —
remotePatterns설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼
모바일 헤더 backdrop-filter 깜빡임 수정 (2026-02-11)
현상: 모바일(Safari/Chrome)에서 sticky 헤더가 스크롤 시 투명↔불투명 깜빡임 발생. PC 브라우저 축소로는 재현 불가, 실제 모바일 기기에서만 발생.
원인 2가지:
globals.css에* { transition: all 0.2s }— 전체 요소의 모든 CSS 속성에 전역 transition. 모바일 스크롤 리페인트 시 background/opacity가 매번 애니메이션- 모바일 헤더의
clean-glass클래스:backdrop-filter: blur(8px)+background: rgba(255,255,255, 0.95)조합이 모바일 sticky 요소에서 GPU 컴포지팅 충돌
수정:
globals.css:*전역 transition →button, a, input, select, textarea, [role]인터랙티브 요소만,transition: all→color, background-color, border-color, box-shadow속성만- 모바일 헤더:
clean-glass(반투명+blur) →bg-background border border-border(불투명 배경)
교훈:
transition: all은 절대*에 걸지 않기. 모바일 성능 저하 + 의도치 않은 애니메이션 발생backdrop-filter: blur()+sticky조합은 모바일 브라우저 고질적 리페인트 버그. 모바일 헤더는 불투명 배경 사용- 0.95 투명도는 육안 구분 불가 → 불투명 처리해도 시각적 차이 없음
사용처 (9개 파일):
| 파일 | 용도 | 이미지 소스 |
|---|---|---|
DocumentHeader.tsx (2건) |
문서 헤더 로고 | logo.imageUrl (API) |
ProductInspectionInputModal.tsx |
제품검사 사진 미리보기 | blob URL |
ProductInspectionDocument.tsx |
제품검사 문서 | data.productImage (API) |
inspection-shared.tsx |
검사 기준서 이미지 | standardImage (API) |
SlatInspectionContent.tsx |
도해 이미지 | schematicImage (API) |
ScreenInspectionContent.tsx |
도해 이미지 | schematicImage (API) |
BendingInspectionContent.tsx |
도해 이미지 | schematicImage (API) |
SlatJointBarInspectionContent.tsx |
도해 이미지 | schematicImage (API) |
BendingWipInspectionContent.tsx |
도해 이미지 | schematicImage (API) |
참고: next/image가 유효한 케이스는 공개 사이트 + 정적/내부 이미지 + SEO 중요한 상황
next/dynamic 코드 스플리팅 적용 (2026-02-10)
결정: 대형 컴포넌트 + 무거운 라이브러리에 next/dynamic / 동적 import() 적용
핵심 개념 — Suspense vs dynamic():
Suspense+ 정적 import → 코드가 부모와 같은 번들 청크에 포함. 유저가 안 봐도 이미 다운로드됨. UI fallback만 제공하고 코드 분할은 안 일어남dynamic()→ webpack이 별도.js청크로 분리. 컴포넌트가 실제 렌더될 때만 네트워크 요청으로 해당 청크 다운로드. 진짜 코드 분할
적용 내역:
| 파일 | 대상 | 절감 |
|---|---|---|
reports/comprehensive-analysis/page.tsx |
MainDashboard (2,651줄 + recharts) | ~350KB |
components/business/Dashboard.tsx |
CEODashboard | ~200KB |
construction/ConstructionDashboard.tsx |
ConstructionMainDashboard | ~100KB |
production/dashboard/page.tsx |
ProductionDashboard | ~100KB |
lib/utils/excel-download.ts |
xlsx 라이브러리 (~400KB) | ~400KB |
quotes/LocationListPanel.tsx |
xlsx 직접 import 제거 | (위와 중복) |
xlsx 동적 로드 패턴:
// Before: 모든 페이지에 xlsx ~400KB 포함
import * as XLSX from 'xlsx';
// After: 엑셀 버튼 클릭 시에만 로드
async function loadXLSX() {
return await import('xlsx');
}
export async function downloadExcel(...) {
const XLSX = await loadXLSX();
// ...
}
총 절감: 초기 번들에서 ~850KB 제외 (대시보드 미방문 + 엑셀 미사용 시)
테이블 가상화 (react-window) — 보류 (2026-02-10)
결정: 현시점 도입 불필요, 성능 이슈 발생 시 검토
근거:
- 페이지네이션 사용 중 — 리스트 페이지 대부분 서버 사이드 페이지네이션 (20~50건/페이지). 50개
<tr>은 브라우저가 문제없이 처리 - 적용 복잡도 높음 — 테이블 헤더 고정, 체크박스 선택,
rowSpan/colSpan병합 등 기존 기능과 충돌 가능. DataTable + IntegratedListTemplateV2 + UniversalListPage 전부 수정 필요 - YAGNI — 500건 이상 한 번에 렌더링하는 페이지가 현재 없음
도입 시점: 한 페이지에 200건+ 데이터를 페이지네이션 없이 표시해야 하는 요구가 생길 때
SWR / React Query — 보류 (2026-02-10)
결정: 현시점 도입 불필요, 성능 이슈 발생 시 검토
근거:
- 기존 패턴 안정화 완료 —
useEffect+ Server Action 호출 패턴이 전 페이지에 일관 적용됨 - 캐싱 니즈 낮음 — 폐쇄형 ERP 특성상 항상 최신 데이터 필요. stale 데이터 표시는 오히려 위험
- 마스터데이터 캐싱 구현됨 — Zustand (
stores/masterDataStore)로 변경 빈도 낮은 데이터는 이미 캐싱 중 - 도입 비용 과다 — 수십 개 페이지
useState+useEffect패턴 전면 리팩토링 + 팀 학습 비용
도입 시점: 동일 데이터를 여러 컴포넌트에서 동시 요구하거나, 목록 ↔ 상세 이동 시 재로딩이 체감될 때
컴포넌트 레지스트리 관계도 (2026-02-12)
구현: /dev/component-registry 페이지에 관계도(카드형 플로우) 뷰 추가
구성:
actions.ts—extractComponentImports()+buildRelationships()로 import 관계 양방향 파싱 (imports/usedBy)ComponentRelationshipView.tsx— 3칼럼 카드형 플로우 (사용처 → 선택 컴포넌트 → 구성요소)ComponentRegistryClient.tsx— 목록/관계도 뷰 토글
활용 규칙 (CLAUDE.md에 추가됨):
- 새 컴포넌트 생성 전 → 목록에서 중복 검색 + 관계도에서 조합 패턴 확인
- 기존 컴포넌트 수정 시 → usedBy로 영향 범위 파악
Action 팩토리 패턴 — 신규 CRUD 적용 규칙 (2026-02-10)
결정: 기존 84개 actions.ts 전면 전환은 하지 않음. 신규 CRUD 도메인에만 팩토리 사용
현황:
src/lib/api/create-crud-service.ts(177줄) — CRUD 보일러플레이트 자동 생성 팩토리- 현재 사용 중: TitleManagement, RankManagement (2개)
- 전환 가능: 15~20개 / 전환 불가 (커스텀 로직): 50+개
규칙:
- 신규 도메인 추가 시 단순 CRUD →
createCrudService사용 필수 - 기존 actions.ts는 잘 동작하므로 무리하게 전환하지 않음
- 커스텀 비즈니스 로직이 있는 도메인(견적, 수주, 생산 등)은 팩토리 비적합
사용 예시:
import { createCrudService } from '@/lib/api/create-crud-service';
const service = createCrudService<ApiData, FrontendType>({
basePath: '/api/v1/resources',
transform: (api) => ({ id: api.id, name: api.name }),
entityName: '리소스',
});
export const getList = service.getList;
export const getById = service.getById;
export const create = service.create;
export const update = service.update;
export const remove = service.remove;
미전환 사유: 84개 중 전환 가능 1520개, 작업 24시간 대비 기능 변화 없음. 시간 대비 효율 낮음
Server Action 공통 유틸리티 — 전체 마이그레이션 완료 (2026-02-12)
결정: buildApiUrl() 전체 43개 actions.ts에 적용 완료
배경:
- 89개 actions.ts 중 43개에서 동일한 URLSearchParams 조건부
.set()패턴 반복 (326+ 건) - 50+ 파일에서
current_page → currentPage수동 변환 반복 toPaginationMeta가src/lib/api/types.ts에 존재하나 import 0건
생성된 유틸리티:
src/lib/api/query-params.ts—buildQueryParams(),buildApiUrl(): URLSearchParams 보일러플레이트 제거src/lib/api/execute-paginated-action.ts—executePaginatedAction(): 페이지네이션 조회 패턴 통합 (내부에서toPaginationMeta사용)
마이그레이션 결과 (2026-02-12):
new URLSearchParams사용: 326건 → 0건 (actions.ts 기준)const API_URL = process.env.NEXT_PUBLIC_API_URL선언: 43개 → 0개 (마이그레이션 대상 파일)buildApiUrl()import: 43개 actions.ts 전체 적용- 3가지 API_URL 패턴 통합: 표준(
process.env),/api접미사(HR),API_BASE전체경로(품질) → 모두buildApiUrl('/api/v1/...')통일
executePaginatedAction 마이그레이션 (2026-02-12):
- 14개 actions.ts에서 페이지네이션 목록 조회 함수를
executePaginatedAction으로 전환 - Wave A (accounting 9개): BillManagement, DepositManagement, SalesManagement, PurchaseManagement, WithdrawalManagement, VendorLedger, CardTransactionInquiry, BankTransactionInquiry, ExpectedExpenseManagement
- Wave B (5개): PaymentHistoryManagement, StockStatus, ReceivingManagement, ShipmentManagement, quotes
- 제외 5개: AccountManagement(
meta필드명), orders(data.items중첩), VacationManagement, EmployeeManagement, construction/order-management (별도 구조) - 순 감소: ~220줄 (14파일 × ~20줄 제거, ~28줄 추가)
- 제거된 보일러플레이트:
DEFAULT_PAGINATION,FrontendPagination/PaginationMeta로컬 인터페이스,PaginatedApiResponseimport, 수동 transform+pagination 조립 - 화면 검수 완료 (4개 페이지): Bills, StockStatus, Quotes, Shipments — 전체 PASS
- 버그 발견/수정:
quotes/actions.ts에서export type { PaginationMeta }re-export가 Turbopack 런타임 에러 유발 (tsc로 미감지) → re-export 제거, 컴포넌트에서@/lib/api/types직접 import로 변경
'use server' 파일 타입 export 제한 (2026-02-12)
발견 배경: executePaginatedAction 마이그레이션 화면 검수 중 견적관리 페이지 빌드 에러
제한 사항:
'use server'파일에서는 async 함수만 export 가능 (Next.js Turbopack 제한)export type { X } from '...'(re-export) → 런타임 에러 발생export interface X { ... }/export type X = ...(인라인 정의) → 문제 없음 (컴파일 시 제거)tsc --noEmit으로는 감지 불가 — Next.js 전용 규칙이므로 실제 페이지 접속(Turbopack)에서만 발생
현재 상태: 전체 81개 'use server' 파일 점검 완료, re-export 패턴 0건 (수정된 1건 포함)
buildApiUrl 마이그레이션 전략:
- Wave A: 1건짜리 단순 파일 20개
- Wave B: 2건짜리 파일 12개 (quotes, WorkOrders, orders 등 대형 파일 포함)
- Wave C: 3건 이상 파일 12개 (VendorLedger 5건, ReceivingManagement 5건, ProcessManagement 19건 URL 등)
효과:
- 페이지네이션 조회 코드: ~20줄 → ~5줄
DEFAULT_PAGINATION중앙화 (execute-paginated-action.ts내부)toPaginationMeta자동 활용 (직접 import 불필요)- URL 빌딩 패턴 완전 일관화 (undefined/null/'' 자동 필터링, boolean/number 자동 변환)
KST 안전 날짜 유틸리티 — toISOString 사용 금지 (2026-02-19)
현황: new Date().toISOString().split('T')[0] — 15개 파일 26곳에서 사용 중이었음
문제: toISOString()은 UTC 기준으로 변환. 한국(KST, UTC+9)에서 오전 9시 이전에 실행하면 전날 날짜 반환
// 2026-02-19 08:30 KST → UTC는 2026-02-18 23:30
new Date().toISOString().split('T')[0] // "2026-02-18" ← 잘못됨
결정: KST 안전 유틸리티 함수로 전량 교체, 직접 toISOString 사용 금지
유틸리티 (src/lib/utils/date.ts):
| 함수 | 용도 | 대체 대상 |
|---|---|---|
getTodayString() |
오늘 날짜 문자열 | new Date().toISOString().split('T')[0] |
getLocalDateString(date) |
임의 Date 객체 문자열 | someDate.toISOString().split('T')[0] |
사용 규칙:
// 올바른 패턴
import { getTodayString, getLocalDateString } from '@/lib/utils/date';
const today = getTodayString(); // "2026-02-19"
const thirtyDaysAgo = getLocalDateString(pastDate); // "2026-01-20"
// 금지 패턴
const today = new Date().toISOString().split('T')[0];
현재 상태: src/ 내 toISOString().split 사용 0건 (date.ts 내 구현부 제외)
달력/스케줄 공통 리소스 — 작업 전 필수 확인 (2026-02-23)
달력·일정·날짜 관련 작업 시 아래 공통 리소스를 반드시 확인하고 사용할 것.
날짜 유틸리티 (src/lib/utils/date.ts):
| 함수 | 용도 |
|---|---|
getLocalDateString(date) |
Date → 'YYYY-MM-DD' (KST 안전) |
getTodayString() |
오늘 날짜 문자열 |
formatDate(dateStr) |
표시용 날짜 포맷 (null → '-') |
formatDateForInput(dateStr) |
input용 YYYY-MM-DD 변환 |
formatDateRange(start, end) |
'시작 ~ 종료' 포맷 |
getDateAfterDays(n) |
N일 후 날짜 |
달력 일정 스토어 (src/stores/useCalendarScheduleStore.ts):
- 달력관리(CalendarManagement)에서 등록한 공휴일/세무일정/회사일정을 프로젝트 전체에 공유
fetchSchedules(year)— 연도별 캐시 조회 (API 호출)setSchedulesForYear(year, data)— 이미 가져온 데이터 직접 설정invalidateYear(year)— 캐시 무효화 (등록/수정/삭제 후)- 현재 상태: 백엔드 API 미구현 → 호출부 주석 처리 (TODO 검색)
달력 이벤트 유틸 (src/constants/calendarEvents.ts):
isHoliday(date),isTaxDeadline(date),getHolidayName(date)등- 스토어 우선 → 하드코딩 폴백(2026년) 패턴
- 새 연도 폴백 데이터 필요 시 이 파일에
HOLIDAYS_YYYY,TAX_DEADLINES_YYYY추가
ScheduleCalendar 공통 컴포넌트 (src/components/common/ScheduleCalendar/):
hideNavigationprop으로 헤더 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시)availableViews={[]}으로 뷰 전환 버튼 숨김
규칙:
Date → string변환 시getLocalDateString()필수 (toISOString().split('T')[0]금지)- 공휴일/세무일 판별 시
calendarEvents.ts유틸 함수 사용 - 달력 데이터 공유 시 zustand 스토어 경유 (컴포넌트 간 직접 전달 금지)
useDateRange 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19)
현황: 20+ 리스트 페이지에서 useState('2025-01-01') / useState('2025-12-31') 하드코딩
문제: 연도가 바뀌면 수동으로 모든 파일 수정 필요 (2025→2026 전환 시 데이터 미표시 버그 발생)
결정: useDateRange 훅으로 동적 날짜 범위 자동 계산
훅 (src/hooks/useDateRange.ts):
import { useDateRange } from '@/hooks';
// 프리셋
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear'); // 2026-01-01 ~ 2026-12-31
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentMonth'); // 2026-02-01 ~ 2026-02-28
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('today'); // 2026-02-19 ~ 2026-02-19
적용 규칙:
- 리스트 페이지 날짜 필터 →
useDateRange필수 사용 - 연간 조회 →
'currentYear', 월간 조회 →'currentMonth' useState('YYYY-MM-DD')하드코딩 금지
현재 상태: useState('2025 패턴 0건 (전량 useDateRange로 전환 완료)
Zod 스키마 검증 — 신규 폼 적용 규칙 (2026-02-11)
결정: 기존 폼은 건드리지 않음. 신규 폼에만 Zod + zodResolver 적용
설치 상태: zod@^4.1.12, @hookform/resolvers@^5.2.2 — 이미 설치됨
효과:
- 스키마 하나로 타입 추론 + 런타임 검증 동시 해결 (
z.infer<typeof schema>) - 별도
interface중복 정의 불필요 - 신규 코드에서
as캐스트 자연 감소 (D-2 개선 효과)
규칙:
- 신규 폼 →
zodResolver(schema)사용 필수 (CLAUDE.md에 패턴 명시) - 기존
rules={{ required: true }}패턴 폼 → 마이그레이션 불필요 - 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링)
미적용 사유: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산