Files
sam-react-prod/claudedocs/architecture/[REF] technical-decisions.md
유병철 07374c826c refactor(WEB): claudedocs 재정리 및 AuthContext/Zustand/유틸 코드 개선
- 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>
2026-02-23 17:17:13 +09:00

17 KiB
Raw Blame History

프로젝트 기술 결정 사항

_index.md에서 분리됨 (2026-02-23). 프로젝트 전반의 기술 선택 배경과 근거를 기록.


<img> 태그 사용 — next/image 미사용 이유 (2026-02-10)

현황: 프로젝트 전체 <img> 태그 10건, next/image 0건

결정: <img> 유지, next/image 전환 불필요

근거:

  1. 폐쇄형 ERP 시스템 — SEO 불필요, LCP 점수 무의미
  2. 전량 외부 동적 이미지 — 백엔드 API에서 받아오는 URL (정적 내부 이미지 0건)
  3. 프린트/문서 레이아웃 — 10건 중 8건이 검사 기준서·도해 등 인쇄용. next/imagewidth/height 강제 지정이 프린트 레이아웃을 깰 위험
  4. blob URL 비호환 — 업로드 미리보기(blob:)는 next/image가 지원 안 함
  5. 설정 부담 > 이점remotePatterns 설정 + 백엔드 도메인 관리 비용이 실질 이점보다 큼

모바일 헤더 backdrop-filter 깜빡임 수정 (2026-02-11)

현상: 모바일(Safari/Chrome)에서 sticky 헤더가 스크롤 시 투명↔불투명 깜빡임 발생. PC 브라우저 축소로는 재현 불가, 실제 모바일 기기에서만 발생.

원인 2가지:

  1. globals.css* { transition: all 0.2s } — 전체 요소의 모든 CSS 속성에 전역 transition. 모바일 스크롤 리페인트 시 background/opacity가 매번 애니메이션
  2. 모바일 헤더의 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: allcolor, 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)

결정: 현시점 도입 불필요, 성능 이슈 발생 시 검토

근거:

  1. 페이지네이션 사용 중 — 리스트 페이지 대부분 서버 사이드 페이지네이션 (20~50건/페이지). 50개 <tr>은 브라우저가 문제없이 처리
  2. 적용 복잡도 높음 — 테이블 헤더 고정, 체크박스 선택, rowSpan/colSpan 병합 등 기존 기능과 충돌 가능. DataTable + IntegratedListTemplateV2 + UniversalListPage 전부 수정 필요
  3. YAGNI — 500건 이상 한 번에 렌더링하는 페이지가 현재 없음

도입 시점: 한 페이지에 200건+ 데이터를 페이지네이션 없이 표시해야 하는 요구가 생길 때

SWR / React Query — 보류 (2026-02-10)

결정: 현시점 도입 불필요, 성능 이슈 발생 시 검토

근거:

  1. 기존 패턴 안정화 완료useEffect + Server Action 호출 패턴이 전 페이지에 일관 적용됨
  2. 캐싱 니즈 낮음 — 폐쇄형 ERP 특성상 항상 최신 데이터 필요. stale 데이터 표시는 오히려 위험
  3. 마스터데이터 캐싱 구현됨 — Zustand (stores/masterDataStore)로 변경 빈도 낮은 데이터는 이미 캐싱 중
  4. 도입 비용 과다 — 수십 개 페이지 useState+useEffect 패턴 전면 리팩토링 + 팀 학습 비용

도입 시점: 동일 데이터를 여러 컴포넌트에서 동시 요구하거나, 목록 ↔ 상세 이동 시 재로딩이 체감될 때

컴포넌트 레지스트리 관계도 (2026-02-12)

구현: /dev/component-registry 페이지에 관계도(카드형 플로우) 뷰 추가

구성:

  • actions.tsextractComponentImports() + 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 수동 변환 반복
  • toPaginationMetasrc/lib/api/types.ts에 존재하나 import 0건

생성된 유틸리티:

  1. src/lib/api/query-params.tsbuildQueryParams(), buildApiUrl(): URLSearchParams 보일러플레이트 제거
  2. src/lib/api/execute-paginated-action.tsexecutePaginatedAction(): 페이지네이션 조회 패턴 통합 (내부에서 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 로컬 인터페이스, PaginatedApiResponse import, 수동 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/):

  • hideNavigation prop으로 헤더 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시)
  • 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 — 이미 설치됨

효과:

  1. 스키마 하나로 타입 추론 + 런타임 검증 동시 해결 (z.infer<typeof schema>)
  2. 별도 interface 중복 정의 불필요
  3. 신규 코드에서 as 캐스트 자연 감소 (D-2 개선 효과)

규칙:

  • 신규 폼 → zodResolver(schema) 사용 필수 (CLAUDE.md에 패턴 명시)
  • 기존 rules={{ required: true }} 패턴 폼 → 마이그레이션 불필요
  • 단순 1~2 필드 인라인 폼 → Zod 불필요 (오버엔지니어링)

미적용 사유: 기존 폼 수십 개를 전면 전환하는 비용 >> 이득. 신규 코드에서 점진적 확산