diff --git a/claudedocs/_index.md b/claudedocs/_index.md index e66f8d7a..e73069c7 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -179,10 +179,12 @@ claudedocs/ | ํŒŒ์ผ | ์„ค๋ช… | |------|------| -| `[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | ๐Ÿ”ด **NEW** - masterDataStore ์บ์‹œ ํ…Œ๋„ŒํŠธ ๊ฒฉ๋ฆฌ ์ˆ˜์ • (page_config ํ‚ค์— tenantId ์ถ”๊ฐ€, dead code ํ•ด์†Œ) | +| `[PLAN-2026-02-06] refactoring-roadmap.md` | ๐Ÿ”ด **NEW** - ๋ฆฌํŒฉํ† ๋ง ์ข…ํ•ฉ ๋กœ๋“œ๋งต (5 Phase, ๊ณตํ†ตํ›…~์„ฑ๋Šฅ์ตœ์ ํ™”, **์ „๋ถ€ ํ”„๋ก ํŠธ ๋‹จ๋…**) | +| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | ๐Ÿ”ด **NEW** - ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ณตํ†ตํ™”/์ตœ์ ํ™” ์ข…ํ•ฉ ๋กœ๋“œ๋งต (8 Phase, API ํ…Œ๋„ŒํŠธ ์ฃผ์ž…~๋ผ์šฐํŒ…) | +| `[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore ์บ์‹œ ํ…Œ๋„ŒํŠธ ๊ฒฉ๋ฆฌ ์ˆ˜์ • (page_config ํ‚ค์— tenantId ์ถ”๊ฐ€, dead code ํ•ด์†Œ) | | `[PLAN-2025-12-29] dynamic-menu-refresh.md` | ๋™์  ๋ฉ”๋‰ด ๊ฐฑ์‹  ์‹œ์Šคํ…œ (1๋‹จ๊ณ„: ํด๋ง, 2๋‹จ๊ณ„: SSE) | -| `multi-tenancy-implementation.md` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ | -| `multi-tenancy-test-guide.md` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ…Œ์ŠคํŠธ | +| `multi-tenancy-implementation.md` | โœ… **Phase 1-2 ์™„๋ฃŒ** - ์ดˆ๊ธฐ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ (AuthContext, TenantAwareCache) | +| `multi-tenancy-test-guide.md` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์บ์‹œ ๊ฒฉ๋ฆฌ ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ | | `architecture-integration-risks.md` | ํ†ตํ•ฉ ๋ฆฌ์Šคํฌ | | `browser-support-policy.md` | ๋ธŒ๋ผ์šฐ์ € ์ง€์› ์ •์ฑ… | | `ssr-hydration-fix.md` | SSR ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์ˆ˜์ • | diff --git a/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md b/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md new file mode 100644 index 00000000..e6c9c41b --- /dev/null +++ b/claudedocs/architecture/[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md @@ -0,0 +1,666 @@ +# ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ณตํ†ตํ™” ๋ฐ ์ตœ์ ํ™” ๋กœ๋“œ๋งต + +**์ž‘์„ฑ์ผ**: 2026-02-06 +**๋ชฉ์ **: ์ „์ฒด ํ”„๋กœ์ ํŠธ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ค€๋น„ ์ƒํƒœ ์ ๊ฒ€ ๋ฐ ๊ณตํ†ตํ™”/์ตœ์ ํ™” ๊ณ„ํš ์ˆ˜๋ฆฝ +**์ด์ „ ๋ฌธ์„œ**: `[REF-2025-11-19] multi-tenancy-implementation.md` (Phase 1-2 ์™„๋ฃŒ) + +--- + +## ํ˜„์žฌ ์ƒํƒœ ์š”์•ฝ (2026-02-06 ๊ธฐ์ค€) + +### ์™„๋ฃŒ๋œ ํ•ญ๋ชฉ (์ด์ „ ๋กœ๋“œ๋งต Phase 1-2) + +| ํ•ญ๋ชฉ | ์ƒํƒœ | ํŒŒ์ผ | +|------|------|------| +| User ํƒ€์ž…์— Tenant ๊ฐ์ฒด ํฌํ•จ | โœ… ์™„๋ฃŒ | `src/contexts/AuthContext.tsx` | +| Tenant ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ (id, company_name ๋“ฑ) | โœ… ์™„๋ฃŒ | `src/contexts/AuthContext.tsx` | +| TenantAwareCache ์œ ํ‹ธ๋ฆฌํ‹ฐ | โœ… ์™„๋ฃŒ | `src/lib/cache/TenantAwareCache.ts` | +| ํ…Œ๋„ŒํŠธ ์ „ํ™˜ ๊ฐ์ง€ + ์บ์‹œ ํด๋ฆฌ์–ด | โœ… ์™„๋ฃŒ | `src/contexts/AuthContext.tsx` | +| masterDataStore ํ…Œ๋„ŒํŠธ ์Šค์ฝ”ํ”„ ์บ์‹œ ํ‚ค | โœ… ์™„๋ฃŒ | `src/stores/masterDataStore.ts` | +| sessionStorage/localStorage ํ…Œ๋„ŒํŠธ ๊ฒฉ๋ฆฌ | โœ… ์™„๋ฃŒ | `mes-{tenantId}-{key}` ํŒจํ„ด | + +### ๋ฏธ์™„๋ฃŒ / ๊ฐœ์„  ํ•„์š” ํ•ญ๋ชฉ + +| ์˜์—ญ | ์šฐ์„ ์ˆœ์œ„ | ํ˜„์žฌ ์ƒํƒœ | +|------|----------|-----------| +| API ํ”„๋ก์‹œ์— ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ ์ „๋‹ฌ | ๐Ÿ”ด | X-Tenant-ID ํ—ค๋” ์—†์Œ | +| Server Actions ํ…Œ๋„ŒํŠธ ์ธ์‹ | ๐Ÿ”ด | 70+ actions.ts์— ํ…Œ๋„ŒํŠธ ๋ฏธํฌํ•จ | +| ํฌ๋งคํ„ฐ/์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‹ค๊ตญ์–ด/๋‹คํ†ตํ™” | ๐Ÿ”ด | ํ•œ๊ตญ์–ด ํ•˜๋“œ์ฝ”๋”ฉ | +| ๋ธŒ๋žœ๋”ฉ ๋™์ ํ™” (๋กœ๊ณ , ์•ฑ์ด๋ฆ„) | ๐ŸŸก | "SAM", sam-logo.png ํ•˜๋“œ์ฝ”๋”ฉ | +| ์ƒ์ˆ˜/๊ณตํœด์ผ ์™ธ๋ถ€ํ™” | ๐ŸŸก | ํ•œ๊ตญ ๊ณตํœด์ผ ํ•˜๋“œ์ฝ”๋”ฉ | +| localStorage ์ง์ ‘ ์‚ฌ์šฉ ์ž”์žฌ | ๐ŸŸก | TenantAwareCache ๋ฏธ์‚ฌ์šฉ ๊ณณ ์กด์žฌ | +| tenantId ํƒ€์ž… ๋ถˆ์ผ์น˜ | ๐ŸŸก | string vs number ํ˜ผ์žฌ | +| ํ…Œ๋„ŒํŠธ ๋ผ์šฐํŒ… | ๐ŸŸข | ํ˜„์žฌ ์—†์Œ (ํ•„์š” ์‹œ ์ถ”๊ฐ€) | +| TenantContext Provider | ๐ŸŸข | ํ…Œ๋„ŒํŠธ ์„ค์ • ์ „์šฉ Context ์—†์Œ | + +--- + +## ์ž‘์—… ์˜์—ญ ๊ตฌ๋ถ„: ํ”„๋ก ํŠธ ๋‹จ๋… vs ๋ฐฑ์—”๋“œ ํ˜‘์˜ + +### ์„ ํ–‰ ํ™•์ธ ์‚ฌํ•ญ + +> **ํ•ต์‹ฌ ์งˆ๋ฌธ**: "๋ฐฑ์—”๋“œ๊ฐ€ ์ด๋ฏธ JWT ํ† ํฐ ์•ˆ์˜ tenant_id๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  ์žˆ๋Š”๊ฐ€?" +> +> - **Yes** โ†’ ํ”„๋ก ํŠธ์—์„œ ๋ณ„๋„ X-Tenant-ID ์•ˆ ๋ณด๋‚ด๋„ ๋จ. Phase 1์€ ๋ถˆํ•„์š”ํ•˜๊ณ  ํ”„๋ก ํŠธ ๋‹จ๋… Phase๋ถ€ํ„ฐ ์ง„ํ–‰ +> - **No** โ†’ ๋ฐฑ์—”๋“œ๋„ ๊ฐ™์ด ์ˆ˜์ • ํ•„์š”. Phase 1์ด ์ตœ์šฐ์„  + +### ํ”„๋ก ํŠธ ๋‹จ๋… ๊ฐ€๋Šฅ (๋ฐฑ์—”๋“œ ์ˆ˜์ • ๋ถˆํ•„์š”) + +| Phase | ์ž‘์—… | ์ด์œ  | +|-------|------|------| +| **3** | ํฌ๋งคํ„ฐ ๋‹ค๊ตญ์–ด/๋‹คํ†ตํ™” ์ „ํ™˜ | `formatAmount()`, `formatDate()` ๋“ฑ ํ”„๋ก ํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋‚ด๋ถ€ ์ˆ˜์ •. ๊ธฐ๋ณธ๊ฐ’์„ ํ•œ๊ตญ์–ด๋กœ ์œ ์ง€ํ•˜๋ฉด ํ•˜์œ„ ํ˜ธํ™˜ | +| **6** | localStorage ์ž”์žฌ ์ •๋ฆฌ + tenantId ํƒ€์ž… ํ†ต์ผ | ํ”„๋ก ํŠธ ์ฝ”๋“œ ์ •๋ฆฌ. TenantAwareCache ๋ฏธ์‚ฌ์šฉ ๊ณณ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, `string` โ†’ `number` ํ†ต์ผ | +| **8** | ํ…Œ๋„ŒํŠธ ๋ผ์šฐํŒ… (ํ•„์š” ์‹œ) | Next.js App Router ๊ตฌ์กฐ ๋ณ€๊ฒฝ. ์ˆœ์ˆ˜ ํ”„๋ก ํŠธ ๋ผ์šฐํŒ… | + +> **์ฆ‰์‹œ ์ฐฉ์ˆ˜ ๊ฐ€๋Šฅ**: ๋ฐฑ์—”๋“œ ํ˜‘์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Œ + +### ๋ฐฑ์—”๋“œ ํ˜‘์˜ ํ•„์š” (ํ”„๋ก ํŠธ + ๋ฐฑ์—”๋“œ ๋™์‹œ ์ˆ˜์ •) + +| Phase | ์ž‘์—… | ๋ฐฑ์—”๋“œ ํ•„์š” ์ด์œ  | +|-------|------|-----------------| +| **1** | API ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ ์ฃผ์ž… | ํ”„๋ก ํŠธ์—์„œ `X-Tenant-ID` ํ—ค๋”๋ฅผ ๋ณด๋‚ด๋„, **๋ฐฑ์—”๋“œ๊ฐ€ ์ฝ๊ณ  ํ•„ํ„ฐ๋ง**ํ•ด์ค˜์•ผ ์˜๋ฏธ ์žˆ์Œ. ์•ˆ ์ฝ์œผ๋ฉด ๋ณด๋‚ด๋ดค์ž ๋ฌด์šฉ์ง€๋ฌผ | +| **2** | Server Actions ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | Phase 1์— ์ข…์†. ๋ฐฑ์—”๋“œ๊ฐ€ ํ—ค๋” or URL๋กœ ํ…Œ๋„ŒํŠธ๋ฅผ ๊ตฌ๋ถ„ ์•ˆ ํ•˜๋ฉด ํ”„๋ก ํŠธ๋งŒ ๋ฐ”๊ฟ”๋„ ์†Œ์šฉ์—†์Œ | +| **4** | ๋ธŒ๋žœ๋”ฉ ๋™์ ํ™” | ํ…Œ๋„ŒํŠธ๋ณ„ ๋กœ๊ณ /์•ฑ์ด๋ฆ„์„ **์–ด๋””์„œ ๊ฐ€์ ธ์˜ค๋‚˜?** โ†’ ๋ฐฑ์—”๋“œ API ํ•„์š” (`GET /api/v1/tenant/config`) | +| **5** | ์ƒ์ˆ˜/๊ณตํœด์ผ ์™ธ๋ถ€ํ™” | ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ๋ฅผ **DB์—์„œ ์„œ๋น™**ํ•ด์•ผ ํ•จ โ†’ ๋ฐฑ์—”๋“œ API ํ•„์š” (`GET /api/v1/holidays?year=2026`) | +| **7** | TenantConfigService | ํ…Œ๋„ŒํŠธ ์„ค์ • ํ†ตํ•ฉ API ํ•„์š” โ†’ branding + regional + features๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๋Š” ์—”๋“œํฌ์ธํŠธ | + +### ์ถ”์ฒœ ์ง„ํ–‰ ์ˆœ์„œ + +``` +[์ฆ‰์‹œ ์‹œ์ž‘ - ํ”„๋ก ํŠธ ๋‹จ๋…] + Phase 3 (ํฌ๋งคํ„ฐ) + Phase 6 (localStorage/ํƒ€์ž…) ๋ณ‘๋ ฌ ์ง„ํ–‰ + +[๋ฐฑ์—”๋“œ ํ˜‘์˜ ํ›„ ์‹œ์ž‘] + Phase 1 (API ํ—ค๋”) โ†’ Phase 2 (Actions) + +[๋ฐฑ์—”๋“œ API ์ค€๋น„ ํ›„ ์‹œ์ž‘] + Phase 7 (TenantConfigService) โ†’ Phase 4 (๋ธŒ๋žœ๋”ฉ) + Phase 5 (์ƒ์ˆ˜) +``` + +--- + +## Phase 1: API ๋ ˆ์ด์–ด ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ ์ฃผ์ž… ๐Ÿ”ด `๋ฐฑ์—”๋“œ ํ˜‘์˜ ํ•„์š”` + +> **๋ชฉํ‘œ**: ๋ชจ๋“  ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ์— ํ…Œ๋„ŒํŠธ ์‹๋ณ„ ์ •๋ณด๊ฐ€ ์ „๋‹ฌ๋˜๋„๋ก ํ•จ +> **์˜ˆ์ƒ**: 3-5์ผ + +### 1-1. ๋กœ๊ทธ์ธ ์‹œ tenant_id ์ฟ ํ‚ค ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `src/app/api/auth/login/route.ts` + +**ํ˜„์žฌ**: access_token, refresh_token ์ฟ ํ‚ค๋งŒ ์„ค์ • +**๋ณ€๊ฒฝ**: tenant_id ์ฟ ํ‚ค ์ถ”๊ฐ€ (HttpOnly, API ํ”„๋ก์‹œ์—์„œ ์ฝ๊ธฐ์šฉ) + +```typescript +// ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ์ถ”๊ฐ€ +const tenantCookie = [ + `tenant_id=${data.tenant.id}`, + 'HttpOnly', + ...(isProduction ? ['Secure'] : []), + 'SameSite=Lax', + 'Path=/', + `Max-Age=${data.expires_in || 7200}`, +].join('; '); +response.headers.append('Set-Cookie', tenantCookie); +``` + +### 1-2. API ํ”„๋ก์‹œ์— X-Tenant-ID ํ—ค๋” ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `src/app/api/proxy/[...path]/route.ts` + +**ํ˜„์žฌ**: +```typescript +const headers = { + 'Accept': 'application/json', + 'X-API-KEY': process.env.API_KEY || '', + 'Authorization': `Bearer ${token}`, +}; +``` + +**๋ณ€๊ฒฝ**: +```typescript +const tenantId = request.cookies.get('tenant_id')?.value; +const headers = { + 'Accept': 'application/json', + 'X-API-KEY': process.env.API_KEY || '', + 'Authorization': `Bearer ${token}`, + ...(tenantId && { 'X-Tenant-ID': tenantId }), +}; +``` + +### 1-3. serverFetch ๋ž˜ํผ์— ํ…Œ๋„ŒํŠธ ํ—ค๋” ์ถ”๊ฐ€ + +**ํŒŒ์ผ**: `src/lib/api/fetch-wrapper.ts` + +**ํ˜„์žฌ**: Authorization ํ—ค๋”๋งŒ ์ „๋‹ฌ +**๋ณ€๊ฒฝ**: tenant_id ์ฟ ํ‚ค ์ฝ์–ด์„œ X-Tenant-ID ํ—ค๋” ์ž๋™ ์ถ”๊ฐ€ + +```typescript +export async function serverFetch(url: string, options?: RequestInit) { + const cookieStore = await cookies(); + const token = cookieStore.get('access_token')?.value; + const tenantId = cookieStore.get('tenant_id')?.value; + + const headers = { + ...options?.headers, + 'Authorization': `Bearer ${token}`, + ...(tenantId && { 'X-Tenant-ID': tenantId }), + }; + // ... +} +``` + +### 1-4. ApiClient ํด๋ž˜์Šค์— ํ…Œ๋„ŒํŠธ ์ง€์› + +**ํŒŒ์ผ**: `src/lib/api/client.ts` + +**๋ณ€๊ฒฝ**: `getAuthHeaders()`์— X-Tenant-ID ํฌํ•จ + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] login/route.ts์— tenant_id ์ฟ ํ‚ค Set-Cookie ์ถ”๊ฐ€ +- [ ] proxy/[...path]/route.ts์—์„œ tenant_id ์ฟ ํ‚ค ์ฝ๊ธฐ + X-Tenant-ID ํ—ค๋” ์ „๋‹ฌ +- [ ] fetch-wrapper.ts serverFetch์— X-Tenant-ID ์ž๋™ ์ถ”๊ฐ€ +- [ ] client.ts ApiClient์— tenantId ์˜ต์…˜ ์ถ”๊ฐ€ +- [ ] authenticated-fetch.ts์—๋„ ํ…Œ๋„ŒํŠธ ํ—ค๋” ์ „ํŒŒ ํ™•์ธ +- [ ] ๋กœ๊ทธ์•„์›ƒ ์‹œ tenant_id ์ฟ ํ‚ค ์‚ญ์ œ ํ™•์ธ +- [ ] ํ† ํฐ ๊ฐฑ์‹  ์‹œ tenant_id ์ฟ ํ‚ค ์œ ์ง€ ํ™•์ธ +- [ ] ๋ฐฑ์—”๋“œ์™€ X-Tenant-ID ํ—ค๋” ์ˆ˜์‹  ๋ฐฉ์‹ ํ˜‘์˜ +``` + +--- + +## Phase 2: Server Actions ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๐Ÿ”ด `๋ฐฑ์—”๋“œ ํ˜‘์˜ ํ•„์š”` + +> **๋ชฉํ‘œ**: 70+ actions.ts์—์„œ ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ๊ฐ€ ์ž๋™ ์ „๋‹ฌ๋˜๋„๋ก ํ•จ +> **์˜ˆ์ƒ**: 1-2์ฃผ (Phase 1 ์™„๋ฃŒ ํ›„ ์ž๋™ ์ ์šฉ๋˜๋Š” ๋ถ€๋ถ„ ๋‹ค์ˆ˜) + +### 2-1. ํ˜„์žฌ ํŒจํ„ด ๋ถ„์„ + +๋Œ€๋ถ€๋ถ„์˜ actions.ts๊ฐ€ ์ด ํŒจํ„ด์„ ๋”ฐ๋ฆ„: +```typescript +const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/endpoint`; +const { response, error } = await serverFetch(url, { method: 'GET' }); +``` + +### 2-2. ์ž๋™ ์ ์šฉ ๋ฒ”์œ„ (Phase 1 ์™„๋ฃŒ ์‹œ) + +Phase 1์—์„œ `serverFetch`์— X-Tenant-ID๋ฅผ ์ž๋™ ์ถ”๊ฐ€ํ•˜๋ฉด, **๊ธฐ์กด actions.ts ๋Œ€๋ถ€๋ถ„์€ ์ˆ˜์ • ์—†์ด** ํ…Œ๋„ŒํŠธ ํ—ค๋”๊ฐ€ ์ „๋‹ฌ๋จ. + +### 2-3. ์ˆ˜๋™ ํ™•์ธ ํ•„์š” ์ผ€์ด์Šค + +`serverFetch`๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ `fetch()`๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ณณ: +```bash +# ๊ฒ€์ƒ‰ ๋Œ€์ƒ +grep -r "fetch(" src/components/*/actions.ts --include="*.ts" | grep -v serverFetch +``` + +### 2-4. ์„ ํƒ์  URL ํ…Œ๋„ŒํŠธ ํ”„๋ฆฌํ”ฝ์Šค + +๋ฐฑ์—”๋“œ๊ฐ€ URL ๊ฒฝ๋กœ์— ํ…Œ๋„ŒํŠธ๋ฅผ ์š”๊ตฌํ•˜๋Š” ๊ฒฝ์šฐ๋งŒ: +```typescript +// ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ ์šฉ +function buildTenantUrl(endpoint: string, tenantId?: string): string { + if (endpoint.startsWith('http')) return endpoint; // ๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜ + const base = process.env.NEXT_PUBLIC_API_URL; + return tenantId + ? `${base}/api/v1/tenant/${tenantId}/${endpoint}` + : `${base}/api/v1/${endpoint}`; +} +``` + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] serverFetch ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” actions.ts ๋ชฉ๋ก ํ™•์ธ +- [ ] ์ง์ ‘ fetch() ํ˜ธ์ถœํ•˜๋Š” ๊ณณ serverFetch๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +- [ ] ๋ฐฑ์—”๋“œ์™€ URL ํŒจํ„ด vs ํ—ค๋” ํŒจํ„ด ์ตœ์ข… ํ˜‘์˜ +- [ ] ๊ณ ๋นˆ๋„ ๋„๋ฉ”์ธ ์šฐ์„  ๊ฒ€์ฆ: clients, items, production, sales +- [ ] ์—๋Ÿฌ ์‹œ ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ ๋ˆ„๋ฝ ๋กœ๊ทธ ์ถ”๊ฐ€ +``` + +--- + +## Phase 3: ํฌ๋งคํ„ฐ & ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜ ์ „ํ™˜ ๐Ÿ”ด `ํ”„๋ก ํŠธ ๋‹จ๋… ๊ฐ€๋Šฅ` + +> **๋ชฉํ‘œ**: ํ•œ๊ตญ์–ด ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํฌ๋งคํ„ฐ๋ฅผ ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ +> **์˜ˆ์ƒ**: 3-5์ผ + +### 3-1. ์˜ํ–ฅ๋ฐ›๋Š” ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ํ•จ์ˆ˜ | ํ•˜๋“œ์ฝ”๋”ฉ ๋‚ด์šฉ | +|------|------|---------------| +| `src/utils/formatAmount.ts` | `formatAmount()` | "์›", "๋งŒ์›" | +| `src/utils/formatAmount.ts` | `formatKoreanAmount()` | "์–ต", "๋งŒ" | +| `src/lib/formatters.ts` | `formatBusinessNumber()` | ํ•œ๊ตญ ์‚ฌ์—…์ž๋ฒˆํ˜ธ (XXX-XX-XXXXX) | +| `src/lib/formatters.ts` | `formatPhoneNumber()` | ํ•œ๊ตญ ์ „ํ™” (02-, 010-) | +| `src/utils/date.ts` | `formatDate()` | `'ko-KR'` ๋กœ์ผ€์ผ | + +### 3-2. TenantRegionalConfig ์ธํ„ฐํŽ˜์ด์Šค + +```typescript +// src/types/tenant-config.ts (์‹ ๊ทœ) +export interface TenantRegionalConfig { + locale: string; // 'ko-KR' | 'en-US' | 'ja-JP' + timezone: string; // 'Asia/Seoul' | 'America/New_York' + currency: { + code: string; // 'KRW' | 'USD' | 'JPY' + symbol: string; // '์›' | '$' | 'ยฅ' + locale: string; // Intl.NumberFormat ๋กœ์ผ€์ผ + largeUnitName?: string; // '๋งŒ' (ํ•œ๊ตญ ์ „์šฉ) + largeUnitValue?: number; // 10000 + }; + phone: { + countryCode: string; // '+82' | '+1' | '+81' + format: string; // 'XXX-XXXX-XXXX' + }; + businessNumber: { + format: string; // 'XXX-XX-XXXXX' + label: string; // '์‚ฌ์—…์ž๋ฒˆํ˜ธ' | 'Business No.' | 'ๆณ•ไบบ็•ชๅท' + }; +} +``` + +### 3-3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ‘๊ทผ (ํ•˜์œ„ ํ˜ธํ™˜) + +๊ธฐ์กด ํ•จ์ˆ˜๋ฅผ ๋ฐ”๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ , ์˜ค๋ฒ„๋กœ๋“œ + ๊ธฐ๋ณธ๊ฐ’ ํŒจํ„ด ์ ์šฉ: + +```typescript +// ๊ธฐ์กด ํ˜ธ์ถœ ์ฝ”๋“œ๋ฅผ ๊นจ์ง€ ์•Š๋Š” ๋ฐฉ์‹ +export function formatAmount(amount: number, config?: TenantCurrencyConfig): string { + const cfg = config ?? DEFAULT_KR_CURRENCY_CONFIG; // ๊ธฐ๋ณธ๊ฐ’: ํ•œ๊ตญ + // ... ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜ ํฌ๋งคํŒ… +} +``` + +### 3-4. ๊ธฐ์กด ๊ณตํ†ตํ™” ์ž‘์—… ์ฐธ์กฐ + +**์ด๋ฏธ ์ž‘์„ฑ๋œ ๊ด€๋ จ ๋ฌธ์„œ**: +- `claudedocs/[IMPL-2026-02-05] formatter-commonization-plan.md` +- `claudedocs/[ANALYSIS-2026-01-20] ๊ณตํ†ตํ™”-ํ˜„ํ™ฉ-๋ถ„์„.md` + +์ด ๋ฌธ์„œ๋“ค์˜ ํฌ๋งคํ„ฐ ๊ณตํ†ตํ™” ๊ณ„ํš๊ณผ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ง„ํ–‰. + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] TenantRegionalConfig ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [ ] DEFAULT_KR_CONFIG ๊ธฐ๋ณธ๊ฐ’ ์ƒ์„ฑ (ํ•˜์œ„ ํ˜ธํ™˜) +- [ ] formatAmount() ํ…Œ๋„ŒํŠธ ์„ค์ • ์ง€์› ์ถ”๊ฐ€ +- [ ] formatDate() ํ…Œ๋„ŒํŠธ ๋กœ์ผ€์ผ ์ง€์› ์ถ”๊ฐ€ +- [ ] formatBusinessNumber() ํฌ๋งท ์„ค์ • ์ง€์› ์ถ”๊ฐ€ +- [ ] formatPhoneNumber() ๊ตญ๊ฐ€ ์ฝ”๋“œ ์ง€์› ์ถ”๊ฐ€ +- [ ] ๊ธฐ์กด ํ˜ธ์ถœ ์ฝ”๋“œ ๊นจ์ง€์ง€ ์•Š๋Š”์ง€ ๊ฒ€์ฆ +- [ ] formatter-commonization-plan.md์™€ ํ†ตํ•ฉ +``` + +--- + +## Phase 4: ๋ธŒ๋žœ๋”ฉ ๋™์ ํ™” ๐ŸŸก `๋ฐฑ์—”๋“œ API ํ•„์š”` + +> **๋ชฉํ‘œ**: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ํšŒ์‚ฌ๋ช…/๋กœ๊ณ ๋ฅผ ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ +> **์˜ˆ์ƒ**: 2-3์ผ + +### 4-1. ์˜ํ–ฅ๋ฐ›๋Š” ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ํ•˜๋“œ์ฝ”๋”ฉ | ๋ณ€๊ฒฝ ๋ฐฉํ–ฅ | +|------|----------|-----------| +| `src/layouts/AuthenticatedLayout.tsx` | `APP_NAME = 'SAM'` | `tenant.company_name` ๋˜๋Š” ํ…Œ๋„ŒํŠธ ์„ค์ • | +| `src/layouts/AuthenticatedLayout.tsx` | `` | ํ…Œ๋„ŒํŠธ๋ณ„ ๋กœ๊ณ  URL | +| `src/layouts/AuthenticatedLayout.tsx` | `MOCK_COMPANIES` ๋ฐฐ์—ด | `user.tenant.other_tenants` ์—ฐ๋™ | +| `src/app/[locale]/layout.tsx` | `APP_TITLE = 'SAM - ๋‚ด ์†์•ˆ์˜ ๋Œ€์‹œ๋ณด๋“œ'` | ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜ | +| `src/app/[locale]/layout.tsx` | SEO ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ | ํ…Œ๋„ŒํŠธ๋ณ„ (๋‹จ, ํ์‡„ํ˜•์ด๋ฏ€๋กœ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„) | + +### 4-2. ํ…Œ๋„ŒํŠธ ๋ธŒ๋žœ๋”ฉ ์„ค์ • ๊ตฌ์กฐ + +```typescript +// src/types/tenant-config.ts์— ์ถ”๊ฐ€ +export interface TenantBrandingConfig { + appName: string; // 'SAM' | '์ฃผ์ผ MES' | ์ปค์Šคํ…€ + appSubtitle?: string; // 'Smart Automation Management' + logoUrl: string; // '/sam-logo.png' | '/tenants/282/logo.png' + faviconUrl?: string; + primaryColor?: string; // ํ…Œ๋งˆ ์ฃผ์ƒ‰์ƒ + loginBackground?: string; // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ +} +``` + +### 4-3. ์ ์šฉ ๋ฐฉ์‹ + +```typescript +// AuthenticatedLayout.tsx ๋‚ด๋ถ€ +const { currentUser } = useAuth(); +const branding = currentUser?.tenant?.branding ?? DEFAULT_BRANDING; + +// ๋กœ๊ณ  +{branding.appName} + +// ์•ฑ ์ด๋ฆ„ +

{branding.appName}

+``` + +### 4-4. MOCK_COMPANIES โ†’ other_tenants ์—ฐ๋™ + +**ํ˜„์žฌ**: ํ•˜๋“œ์ฝ”๋”ฉ ๋ชฉ์—… +```typescript +const MOCK_COMPANIES = [ + { id: 'all', name: '์ „์ฒด' }, + { id: 'company1', name: '(์ฃผ)์‚ผ์„ฑ๊ฑด์„ค' }, + ... +]; +``` + +**๋ณ€๊ฒฝ**: ์‹ค์ œ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์—ฐ๋™ +```typescript +const tenantOptions = useMemo(() => { + const current = currentUser?.tenant; + const others = current?.other_tenants ?? []; + return [current, ...others].filter(Boolean); +}, [currentUser]); +``` + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] TenantBrandingConfig ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ +- [ ] DEFAULT_BRANDING ๊ธฐ๋ณธ๊ฐ’ (ํ˜„์žฌ SAM ์„ค์ •) +- [ ] AuthenticatedLayout ๋กœ๊ณ /์•ฑ์ด๋ฆ„ ๋™์ ํ™” +- [ ] MOCK_COMPANIES๋ฅผ other_tenants ๊ธฐ๋ฐ˜์œผ๋กœ ๊ต์ฒด +- [ ] ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ธŒ๋žœ๋”ฉ ๋™์ ํ™” +- [ ] favicon ๋™์  ๋ณ€๊ฒฝ (์„ ํƒ) +- [ ] ํ…Œ๋„ŒํŠธ๋ณ„ ๋กœ๊ณ  ํŒŒ์ผ ์„œ๋น™ ๋ฐฉ์‹ ๊ฒฐ์ • (public/ vs API) +``` + +--- + +## Phase 5: ์ƒ์ˆ˜ & ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์™ธ๋ถ€ํ™” ๐ŸŸก `๋ฐฑ์—”๋“œ API ํ•„์š”` + +> **๋ชฉํ‘œ**: ํ•œ๊ตญ ํŠนํ™” ์ƒ์ˆ˜๋ฅผ ํ…Œ๋„ŒํŠธ/๊ตญ๊ฐ€๋ณ„ ์„ค์ •์œผ๋กœ ์™ธ๋ถ€ํ™” +> **์˜ˆ์ƒ**: 3-5์ผ + +### 5-1. ์˜ํ–ฅ๋ฐ›๋Š” ํ•ญ๋ชฉ + +| ํ•ญ๋ชฉ | ํŒŒ์ผ | ํ˜„์žฌ | ๋ณ€๊ฒฝ | +|------|------|------|------| +| ๊ณตํœด์ผ | `src/constants/calendarEvents.ts` | ํ•œ๊ตญ ๊ณตํœด์ผ ํ•˜๋“œ์ฝ”๋”ฉ | DB/API ๊ธฐ๋ฐ˜ | +| ํ”„๋กœ์„ธ์Šค ํƒ€์ž… | `src/types/process.ts` | "์ƒ์‚ฐ", "๊ฒ€์‚ฌ" ๋“ฑ | i18n ๋ผ๋ฒจ | +| ์ƒํƒœ ๋ผ๋ฒจ | `src/lib/utils/status-config.ts` | "๋Œ€๊ธฐ", "์™„๋ฃŒ" ๋“ฑ | i18n ๋ผ๋ฒจ | +| ํ’ˆ๋ชฉ ํƒ€์ž… | `src/types/item.ts` | "์ œํ’ˆ", "๋ถ€ํ’ˆ" ๋“ฑ | i18n ๋ผ๋ฒจ | +| ๊ทผ๋ฌด์ผ | ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ | ์›”-๊ธˆ ํ•˜๋“œ์ฝ”๋”ฉ | ํ…Œ๋„ŒํŠธ ์„ค์ • | + +### 5-2. ์™ธ๋ถ€ํ™” ์ „๋žต + +**๊ณตํœด์ผ**: ๋ฐฑ์—”๋“œ API๋กœ ์ด๋™ (ํ…Œ๋„ŒํŠธ๋ณ„ ๊ตญ๊ฐ€ ์„ค์ •์— ๋”ฐ๋ผ ๋ฐ˜ํ™˜) +```typescript +// AS-IS: ํ•˜๋“œ์ฝ”๋”ฉ +const HOLIDAYS_2026 = [ + { date: '2026-01-01', name: '์‹ ์ •', type: 'holiday' }, + ... +]; + +// TO-BE: API ํ˜ธ์ถœ +const holidays = await getHolidays(tenantId, year); +``` + +**๋ผ๋ฒจ/์ƒํƒœ**: next-intl ๋‹ค๊ตญ์–ด ์‹œ์Šคํ…œ ํ™œ์šฉ (์ด๋ฏธ ko/en/ja ๊ตฌ์กฐ ์žˆ์Œ) +```typescript +// AS-IS +const statusLabels = { pending: '๋Œ€๊ธฐ', completed: '์™„๋ฃŒ' }; + +// TO-BE +const t = useTranslations('status'); +const label = t('pending'); // ๋กœ์ผ€์ผ์— ๋”ฐ๋ผ ์ž๋™ ๋ณ€ํ™˜ +``` + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] calendarEvents.ts ๊ณตํœด์ผ ๋ฐ์ดํ„ฐ โ†’ API ์—”๋“œํฌ์ธํŠธ๋กœ ์ด๋™ +- [ ] ํ”„๋กœ์„ธ์Šค ํƒ€์ž… ๋ผ๋ฒจ โ†’ messages/ko.json, en.json, ja.json์œผ๋กœ ์ด๋™ +- [ ] ์ƒํƒœ ๋ผ๋ฒจ โ†’ i18n ํ‚ค๋กœ ๋ณ€ํ™˜ +- [ ] ํ’ˆ๋ชฉ ํƒ€์ž… ๋ผ๋ฒจ โ†’ i18n ํ‚ค๋กœ ๋ณ€ํ™˜ +- [ ] ๊ทผ๋ฌด์ผ ์„ค์ • โ†’ ํ…Œ๋„ŒํŠธ config๋กœ ์ด๋™ +- [ ] ๋ฐฑ์—”๋“œ์— ๊ณตํœด์ผ API ์š”์ฒญ +``` + +--- + +## Phase 6: localStorage ์ž”์žฌ ์ •๋ฆฌ & ํƒ€์ž… ํ†ต์ผ ๐ŸŸก `ํ”„๋ก ํŠธ ๋‹จ๋… ๊ฐ€๋Šฅ` + +> **๋ชฉํ‘œ**: TenantAwareCache ๋ฏธ์‚ฌ์šฉ ๊ณณ ์ •๋ฆฌ + tenantId ํƒ€์ž… ํ†ต์ผ +> **์˜ˆ์ƒ**: 2-3์ผ + +### 6-1. localStorage ์ง์ ‘ ์‚ฌ์šฉ ๊ฐ์‚ฌ + +```bash +# ๊ฒ€์ƒ‰ ๋Œ€์ƒ +grep -r "localStorage\.\(setItem\|getItem\)" src/ --include="*.ts" --include="*.tsx" +``` + +**์•Œ๋ ค์ง„ ๋น„-ํ…Œ๋„ŒํŠธ-์Šค์ฝ”ํ”„ ํ‚ค**: +- `mes-users` โ†’ ์‚ฌ์šฉ์ž ๋ชฉ๋ก (ํ…Œ๋„ŒํŠธ ์Šค์ฝ”ํ”„ ํ•„์š” ์—ฌ๋ถ€ ๊ฒ€ํ† ) +- `mes-currentUser` โ†’ ํ˜„์žฌ ์‚ฌ์šฉ์ž (๋กœ๊ทธ์ธ ์ƒํƒœ์ด๋ฏ€๋กœ ํ…Œ๋„ŒํŠธ ๋ฌด๊ด€) +- ๊ธฐํƒ€ ์ง์ ‘ ์‚ฌ์šฉ ๊ณณ โ†’ TenantAwareCache ๋˜๋Š” ํ…Œ๋„ŒํŠธ ํ”„๋ฆฌํ”ฝ์Šค ์ ์šฉ + +### 6-2. tenantId ํƒ€์ž… ํ†ต์ผ + +**ํ˜„์žฌ ์ƒํ™ฉ**: +- `User.tenant.id` โ†’ `number` (AuthContext) +- `PageConfig.tenantId` โ†’ `string` (masterDataStore) +- TenantAwareCache โ†’ `number` + +**ํ†ต์ผ**: `number`๋กœ ํ‘œ์ค€ํ™” (๋ฐฑ์—”๋“œ ์‘๋‹ต ๊ธฐ์ค€) + +```typescript +// ์ˆ˜์ • ๋Œ€์ƒ ์ฐพ๊ธฐ +grep -r "tenantId.*string" src/ --include="*.ts" +``` + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] localStorage ์ง์ ‘ ์‚ฌ์šฉ ์ „์ˆ˜ ์กฐ์‚ฌ +- [ ] TenantAwareCache๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€๋Šฅํ•œ ๊ณณ ๋ชฉ๋กํ™” +- [ ] ํ…Œ๋„ŒํŠธ ์Šค์ฝ”ํ”„ ๋ถˆํ•„์š”ํ•œ ๊ณณ ๋ช…์‹œ (mes-currentUser ๋“ฑ) +- [ ] tenantId: string โ†’ number ํ†ต์ผ +- [ ] PageConfig ํƒ€์ž… ์ˆ˜์ • +- [ ] ๊ด€๋ จ ํƒ€์ž… ์ฐธ์กฐ ์ „๋ถ€ ์—…๋ฐ์ดํŠธ +``` + +--- + +## Phase 7: TenantConfigService & TenantContext (์„ ํƒ) ๐ŸŸข `๋ฐฑ์—”๋“œ API ํ•„์š”` + +> **๋ชฉํ‘œ**: ํ…Œ๋„ŒํŠธ ์„ค์ •์„ ํ•œ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค ๋ ˆ์ด์–ด +> **์˜ˆ์ƒ**: 3-5์ผ (Phase 3-5 ์ง„ํ–‰ ์ค‘ ํ•„์š”์— ๋”ฐ๋ผ) + +### 7-1. TenantConfigService + +```typescript +// src/services/TenantConfigService.ts (์‹ ๊ทœ) +export interface TenantConfiguration { + tenantId: number; + branding: TenantBrandingConfig; + regional: TenantRegionalConfig; + features: { + enabledModules: string[]; + customFields?: Record; + }; + calendar: { + workingDays: number[]; // [1,2,3,4,5] = ์›”-๊ธˆ + holidays: HolidayEntry[]; + }; +} + +class TenantConfigService { + private cache: Map = new Map(); + + async getConfig(tenantId: number): Promise { + if (this.cache.has(tenantId)) return this.cache.get(tenantId)!; + const config = await this.fetchFromApi(tenantId); + this.cache.set(tenantId, config); + return config; + } +} +``` + +### 7-2. TenantContext Provider + +```typescript +// src/contexts/TenantContext.tsx (์‹ ๊ทœ) +export function TenantProvider({ children }: { children: ReactNode }) { + const { currentUser } = useAuth(); + const [config, setConfig] = useState(); + + useEffect(() => { + if (currentUser?.tenant?.id) { + tenantConfigService.getConfig(currentUser.tenant.id) + .then(setConfig); + } + }, [currentUser?.tenant?.id]); + + return ( + + {children} + + ); +} + +// ์‚ฌ์šฉ +const tenantConfig = useTenantConfig(); +const currencySymbol = tenantConfig.regional.currency.symbol; +``` + +### ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +``` +- [ ] TenantConfiguration ํ†ตํ•ฉ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„ +- [ ] TenantConfigService ๊ตฌํ˜„ (์บ์‹œ + API ํ˜ธ์ถœ) +- [ ] TenantContext Provider ๊ตฌํ˜„ +- [ ] useTenantConfig() ํ›… ๊ตฌํ˜„ +- [ ] Protected Layout์— TenantProvider ์ถ”๊ฐ€ +- [ ] ๊ธฐ์กด ์ฝ”๋“œ์—์„œ ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +``` + +--- + +## Phase 8: ํ…Œ๋„ŒํŠธ ๋ผ์šฐํŒ… (ํ•„์š” ์‹œ) ๐ŸŸข `ํ”„๋ก ํŠธ ๋‹จ๋… ๊ฐ€๋Šฅ` + +> **๋ชฉํ‘œ**: URL์— ํ…Œ๋„ŒํŠธ ์‹๋ณ„์ž ํฌํ•จ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ) +> **์˜ˆ์ƒ**: 1์ฃผ+ + +### ํ˜„์žฌ ๋ผ์šฐํŒ… +``` +/[locale]/(protected)/dashboard +``` + +### ์˜ต์…˜ A: ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ (๊ถŒ์žฅ - ํ•„์š” ์‹œ) +``` +/[tenant]/[locale]/(protected)/dashboard +/acme/ko/dashboard +``` + +### ์˜ต์…˜ B: ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ธฐ๋ฐ˜ +``` +acme.sam.com/ko/dashboard +``` + +### ์˜ต์…˜ C: ํ˜„์žฌ ์œ ์ง€ (์ธ์ฆ ๊ธฐ๋ฐ˜๋งŒ) +``` +/[locale]/(protected)/dashboard โ† ํ…Œ๋„ŒํŠธ๋Š” JWT/์ฟ ํ‚ค๋กœ๋งŒ ์‹๋ณ„ +``` + +**๊ฒฐ์ •**: ํ˜„์žฌ๋Š” **์˜ต์…˜ C ์œ ์ง€**. ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ URL ๋ถ„๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด์ง€๋ฉด ์˜ต์…˜ A ๋„์ž…. + +--- + +## ๋ฐฑ์—”๋“œ ํ˜‘์˜ ์‚ฌํ•ญ + +### ํ•„์ˆ˜ ํ˜‘์˜ (Phase 1 ์‹œ์ž‘ ์ „) + +| ํ•ญ๋ชฉ | ์งˆ๋ฌธ | ๊ฒฐ์ • ์‚ฌํ•ญ | +|------|------|-----------| +| ํ…Œ๋„ŒํŠธ ์‹๋ณ„ ๋ฐฉ์‹ | `X-Tenant-ID` ํ—ค๋” vs URL ๊ฒฝ๋กœ vs JWT๋งŒ? | TBD | +| X-Tenant-ID ์ˆ˜์‹  | ๋ฐฑ์—”๋“œ๊ฐ€ ์ด ํ—ค๋”๋ฅผ ์ฝ๊ณ  ํ•„ํ„ฐ๋งํ•˜๋Š”์ง€? | TBD | +| JWT ๋‚ด tenant_id | ํ† ํฐ์— tenant_id๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€? | TBD | +| ๊ณตํœด์ผ API | `GET /api/v1/holidays?year=2026` ์ง€์›? | TBD | +| ํ…Œ๋„ŒํŠธ ์„ค์ • API | `GET /api/v1/tenant/config` ์ง€์›? | TBD | + +### ์„ ํƒ ํ˜‘์˜ (Phase 4-5 ์‹œ์ž‘ ์ „) + +| ํ•ญ๋ชฉ | ์งˆ๋ฌธ | ๊ฒฐ์ • ์‚ฌํ•ญ | +|------|------|-----------| +| ํ…Œ๋„ŒํŠธ ๋กœ๊ณ  | ๋กœ๊ณ  URL์„ ์–ด๋””์„œ ์ œ๊ณต? (API vs ํŒŒ์ผ์„œ๋ฒ„) | TBD | +| ๋ธŒ๋žœ๋”ฉ ์„ค์ • | ํ…Œ๋„ŒํŠธ๋ณ„ ์•ฑ์ด๋ฆ„/ํ…Œ๋งˆ API ์ œ๊ณต ๊ฐ€๋Šฅ? | TBD | +| ๋‹ค๊ตญ์–ด ๋ผ๋ฒจ | ๋ฐฑ์—”๋“œ ์ฝ”๋“œ ๋ผ๋ฒจ์ด ๋‹ค๊ตญ์–ด ์ง€์›? | TBD | + +--- + +## ์‹คํ–‰ ์šฐ์„ ์ˆœ์œ„ ์š”์•ฝ + +``` +[ํ”„๋ก ํŠธ ๋‹จ๋…] Phase 3: ํฌ๋งคํ„ฐ ํ…Œ๋„ŒํŠธ ์„ค์ • ๊ธฐ๋ฐ˜ ๐Ÿ”ด 3-5์ผ โ† ์ฆ‰์‹œ ์‹œ์ž‘ ๊ฐ€๋Šฅ +[ํ”„๋ก ํŠธ ๋‹จ๋…] Phase 6: localStorage ์ •๋ฆฌ/ํƒ€์ž… ํ†ต์ผ ๐ŸŸก 2-3์ผ โ† ์ฆ‰์‹œ ์‹œ์ž‘ ๊ฐ€๋Šฅ +[ํ”„๋ก ํŠธ ๋‹จ๋…] Phase 8: ํ…Œ๋„ŒํŠธ ๋ผ์šฐํŒ… ๐ŸŸข ํ•„์š”์‹œ โ† ๋‹น๋ถ„๊ฐ„ ๋ถˆํ•„์š” + +[๋ฐฑ์—”๋“œ ํ˜‘์˜] Phase 1: API ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ ์ฃผ์ž… ๐Ÿ”ด 3-5์ผ โ† ๋ฐฑ์—”๋“œ ํ™•์ธ ํ›„ +[๋ฐฑ์—”๋“œ ํ˜‘์˜] Phase 2: Server Actions ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๐Ÿ”ด 1-2์ฃผ โ† Phase 1 ํ›„ ์ž๋™ ์ ์šฉ ๋ฒ”์œ„ ํผ + +[๋ฐฑ์—”๋“œ API] Phase 4: ๋ธŒ๋žœ๋”ฉ ๋™์ ํ™” ๐ŸŸก 2-3์ผ โ† ํ…Œ๋„ŒํŠธ ์„ค์ • API ํ•„์š” +[๋ฐฑ์—”๋“œ API] Phase 5: ์ƒ์ˆ˜/๊ณตํœด์ผ ์™ธ๋ถ€ํ™” ๐ŸŸก 3-5์ผ โ† ๊ณตํœด์ผ API ํ•„์š” +[๋ฐฑ์—”๋“œ API] Phase 7: TenantConfigService ๐ŸŸข 3-5์ผ โ† ํ†ตํ•ฉ ์„ค์ • API ํ•„์š” +``` + +### ๋ณ‘๋ ฌ ์ง„ํ–‰ ๊ฐ€๋Šฅ ์กฐํ•ฉ + +``` +[์ฆ‰์‹œ ์‹œ์ž‘ - ํ”„๋ก ํŠธ ๋‹จ๋…] +โ”œโ”€ Phase 3 (ํฌ๋งคํ„ฐ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ ๋…๋ฆฝ ์™„๋ฃŒ +โ””โ”€ Phase 6 (localStorage) โ”€โ”€โ†’ ๋…๋ฆฝ ์™„๋ฃŒ + +[๋ฐฑ์—”๋“œ ํ˜‘์˜ ํ›„ - ํ”„๋ก ํŠธ+๋ฐฑ์—”๋“œ] +โ””โ”€ Phase 1 (API ํ—ค๋”) โ”€โ”€โ”€โ”€โ”€โ”€โ†’ Phase 2 (Actions ์ž๋™ ์ ์šฉ) + +[๋ฐฑ์—”๋“œ API ์ค€๋น„ ํ›„ - ํ”„๋ก ํŠธ+๋ฐฑ์—”๋“œ] +โ””โ”€ Phase 7 (TenantConfig) โ”€โ”€โ†’ Phase 4 (๋ธŒ๋žœ๋”ฉ) + Phase 5 (์ƒ์ˆ˜) +``` + +--- + +## ์œ„ํ—˜ ์š”์†Œ & ๋Œ€์‘ + +| ์œ„ํ—˜ | ํ™•๋ฅ  | ์˜ํ–ฅ | ๋Œ€์‘ | +|------|------|------|------| +| 70+ actions.ts ์ˆ˜๋™ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ | ๋†’์Œ | ์ค‘๊ฐ„ | serverFetch ์ž๋™ ์ฃผ์ž…์œผ๋กœ ๋Œ€๋ถ€๋ถ„ ํ•ด๊ฒฐ | +| ๋ฐฑ์—”๋“œ X-Tenant-ID ๋ฏธ์ง€์› | ์ค‘๊ฐ„ | ๋†’์Œ | Phase 1 ์‹œ์ž‘ ์ „ ๋ฐฑ์—”๋“œ ํŒ€ ํ˜‘์˜ ํ•„์ˆ˜ | +| ํฌ๋งคํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด UI ๊นจ์ง | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ๊ธฐ๋ณธ๊ฐ’ ํŒจํ„ด์œผ๋กœ ํ•˜์œ„ ํ˜ธํ™˜ ์œ ์ง€ | +| ์บ์‹œ ๋ฌดํšจํ™” ๋ˆ„๋ฝ | ๋‚ฎ์Œ | ๋†’์Œ | TenantAwareCache ์ด๋ฏธ ๊ฒ€์ฆ๋จ | +| ๋‹ค๊ตญ์–ด ๋ฒˆ์—ญ ๋ฆฌ์†Œ์Šค ๋ถ€์กฑ | ์ค‘๊ฐ„ | ๋‚ฎ์Œ | ํ•œ๊ตญ์–ด ๊ธฐ๋ณธ๊ฐ’ ์œ ์ง€, ์ ์ง„ ์ถ”๊ฐ€ | + +--- + +## ๊ด€๋ จ ๋ฌธ์„œ + +| ๋ฌธ์„œ | ์„ค๋ช… | +|------|------| +| `architecture/[REF-2025-11-19] multi-tenancy-implementation.md` | ์ด์ „ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ตฌํ˜„ (Phase 1-2 โ†’ ์™„๋ฃŒ๋จ) | +| `architecture/[TEST-2025-11-19] multi-tenancy-test-guide.md` | ์บ์‹œ ๊ฒฉ๋ฆฌ ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ | +| `architecture/[FIX-2026-01-29] masterdata-cache-tenant-isolation.md` | masterDataStore ์บ์‹œ ํ…Œ๋„ŒํŠธ ๊ฒฉ๋ฆฌ ์ˆ˜์ • | +| `[IMPL-2026-02-05] formatter-commonization-plan.md` | ํฌ๋งคํ„ฐ ๊ณตํ†ตํ™” ๊ณ„ํš (Phase 3๊ณผ ๋ณ‘ํ•ฉ) | +| `[ANALYSIS-2026-01-20] ๊ณตํ†ตํ™”-ํ˜„ํ™ฉ-๋ถ„์„.md` | ๊ณตํ†ตํ™” ํ˜„ํ™ฉ ๋ถ„์„ | +| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ณตํ†ตํ™” ํ˜„ํ™ฉ | +| `auth/[IMPL-2025-11-07] jwt-cookie-authentication-final.md` | JWT ์ฟ ํ‚ค ์ธ์ฆ ๊ตฌํ˜„ | +| `api/[REF] api-requirements.md` | API ์š”๊ตฌ์‚ฌํ•ญ | + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|-----------| +| 2026-02-06 | ์ดˆ๊ธฐ ์ž‘์„ฑ - ์ „์ฒด ์ฝ”๋“œ๋ฒ ์ด์Šค ๋ถ„์„ ๊ธฐ๋ฐ˜ 8 Phase ๋กœ๋“œ๋งต | + +--- + +**๋‹ค์Œ ์•ก์…˜**: Phase 1 ์‹œ์ž‘ ์ „ ๋ฐฑ์—”๋“œ ํŒ€๊ณผ `X-Tenant-ID` ํ—ค๋” ์ˆ˜์‹  ๋ฐฉ์‹ ํ˜‘์˜ diff --git a/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md b/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md new file mode 100644 index 00000000..b399dcd5 --- /dev/null +++ b/claudedocs/architecture/[PLAN-2026-02-06] refactoring-roadmap.md @@ -0,0 +1,389 @@ +# ๋ฆฌํŒฉํ† ๋ง ๋กœ๋“œ๋งต + +**์ž‘์„ฑ์ผ**: 2026-02-06 +**๋ชฉ์ **: ์ „์ฒด ์ฝ”๋“œ๋ฒ ์ด์Šค ๋ฆฌํŒฉํ† ๋ง ํฌ์ธํŠธ ์ ๊ฒ€ ๋ฐ ์‹คํ–‰ ๊ณ„ํš +**์ƒํƒœ**: ๋ถ„์„ ์™„๋ฃŒ, ์‹คํ–‰ ๋Œ€๊ธฐ + +--- + +## ํ˜„์žฌ ์ฝ”๋“œ๋ฒ ์ด์Šค ์ˆ˜์น˜ (2026-02-06 ๊ธฐ์ค€) + +| ์ง€ํ‘œ | ์ˆ˜์น˜ | ๋น„๊ณ  | +|------|------|------| +| ์ „์ฒด ์ฝ”๋“œ | ~301,000์ค„ | TS/TSX | +| ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ | ~551๊ฐœ | | +| ํŽ˜์ด์ง€ ํŒŒ์ผ | ~253๊ฐœ | | +| action.ts ํŒŒ์ผ | 80๊ฐœ | ๊ฑฐ์˜ ๋™์ผ CRUD ํŒจํ„ด | +| types.ts ํŒŒ์ผ | 94๊ฐœ | ์ค‘๋ณต ํƒ€์ž… ๋‹ค์ˆ˜ | +| ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ | 42๊ฐœ | ์œ ์‚ฌ ํŒจํ„ด ๋ฐ˜๋ณต | +| 2000์ค„+ ํŒŒ์ผ | 4๊ฐœ | God ์ปดํฌ๋„ŒํŠธ | +| 1000~2000์ค„ ํŒŒ์ผ | 25+๊ฐœ | ๋ถ„๋ฆฌ ๋Œ€์ƒ | +| 500~1000์ค„ ํŒŒ์ผ | 50+๊ฐœ | ๊ฒ€ํ†  ๋Œ€์ƒ | + +--- + +## God ์ปดํฌ๋„ŒํŠธ / ๋Œ€ํ˜• ํŒŒ์ผ ๋ชฉ๋ก + +### ๐Ÿ”ด 2000์ค„ ์ด์ƒ (์ฆ‰์‹œ ๋ถ„๋ฆฌ ํ•„์š”) + +| ํŒŒ์ผ | ์ค„์ˆ˜ | ํ•ต์‹ฌ ๋ฌธ์ œ | ๋ถ„๋ฆฌ ๋ฐฉํ–ฅ | +|------|------|-----------|-----------| +| `components/business/MainDashboard.tsx` | 2,651 | CEO/์˜์—…/์ƒ์‚ฐ/ํ’ˆ์งˆ ๋Œ€์‹œ๋ณด๋“œ ํ•œ ํŒŒ์ผ | ์—ญํ• ๋ณ„ ์„น์…˜ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ | +| `contexts/ItemMasterContext.tsx` | 2,406 | useState 17๊ฐœ, useEffect 15๊ฐœ, ํ•จ์ˆ˜ 50+๊ฐœ | ๋„๋ฉ”์ธ๋ณ„ 5๊ฐœ Context ๋ถ„๋ฆฌ | +| `lib/api/item-master.ts` | 2,232 | ๋ชจ๋“  ํ’ˆ๋ชฉ API ํ•œ ํŒŒ์ผ | ๋„๋ฉ”์ธ๋ณ„ API ๋ชจ๋“ˆ ๋ถ„๋ฆฌ | +| `lib/api/dashboard/transformers.ts` | 1,576 | ์ „์ฒด ๋Œ€์‹œ๋ณด๋“œ ๋ณ€ํ™˜ ๋กœ์ง | ์„น์…˜๋ณ„ transformer ๋ถ„๋ฆฌ | + +### ๐ŸŸก 1000~2000์ค„ (์šฐ์„  ๊ฒ€ํ† ) + +| ํŒŒ์ผ | ์ค„์ˆ˜ | ๋„๋ฉ”์ธ | ๋ถ„๋ฆฌ ๋ฐฉํ–ฅ | +|------|------|--------|-----------| +| `components/orders/actions.ts` | 1,394 | ์ˆ˜์ฃผ | ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ | +| `components/accounting/ExpectedExpenseManagement/index.tsx` | 1,299 | ํšŒ๊ณ„ | ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ ์ถ”์ถœ | +| `layouts/AuthenticatedLayout.tsx` | 1,289 | ๋ ˆ์ด์•„์›ƒ | ํ›… 24๊ฐœ โ†’ ์„น์…˜๋ณ„ ๋ถ„๋ฆฌ | +| `components/quotes/QuoteRegistration.tsx` | 1,268 | ๊ฒฌ์  | ํผ ์„น์…˜ ์ถ”์ถœ, useState 13๊ฐœ | +| `components/quotes/actions.ts` | 1,266 | ๊ฒฌ์  | API ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ | +| `components/business/construction/management/actions.ts` | 1,222 | ๊ฑด์„ค | ๋„๋ฉ”์ธ ์„œ๋น„์Šค ์ถ”์ถœ | +| `components/business/construction/estimates/actions.ts` | 1,222 | ๊ฑด์„ค | ๋„๋ฉ”์ธ ์„œ๋น„์Šค ์ถ”์ถœ | +| `components/production/WorkerScreen/index.tsx` | 1,198 | ์ƒ์‚ฐ | ํ™”๋ฉด ์„น์…˜ ๋ถ„๋ฆฌ | +| `hooks/useCEODashboard.ts` | 1,172 | ๋Œ€์‹œ๋ณด๋“œ | useState 18๊ฐœ โ†’ ์„น์…˜๋ณ„ ํ›… ๋ถ„๋ฆฌ | +| `components/material/ReceivingManagement/actions.ts` | 1,152 | ์ž์žฌ | API ์„œ๋น„์Šค ๋ ˆ์ด์–ด | +| `components/quotes/types.ts` | 1,149 | ๊ฒฌ์  | ํƒ€์ž… ์กฐ์งํ™” | +| `components/quality/InspectionManagement/InspectionDetail.tsx` | 1,125 | ํ’ˆ์งˆ | ์ปดํฌ๋„ŒํŠธ ์ถ”์ถœ | +| `components/hr/VacationManagement/actions.ts` | 1,125 | HR | ์„œ๋น„์Šค ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ | +| `components/orders/OrderRegistration.tsx` | 1,123 | ์ˆ˜์ฃผ | ํผ ์„น์…˜ ์ถ”์ถœ, useState 12๊ฐœ | +| `components/items/DynamicItemForm/index.tsx` | 1,073 | ํ’ˆ๋ชฉ | ๋ณตํ•ฉ ํผ ๋กœ์ง ์ถ”์ถœ | +| `components/templates/IntegratedListTemplateV2.tsx` | 1,066 | ํ…œํ”Œ๋ฆฟ | ํ…œํ”Œ๋ฆฟ ํŠนํ™” | +| `components/hr/EmployeeManagement/EmployeeForm.tsx` | 1,051 | HR | ํผ ์„น์…˜ ๋ถ„๋ฆฌ | +| `components/quotes/QuoteRegistrationV2.tsx` | 1,020 | ๊ฒฌ์  | ํผ ๋ฆฌํŒฉํ† ๋ง | +| `components/templates/UniversalListPage/index.tsx` | 1,007 | ํ…œํ”Œ๋ฆฟ | ํ…œํ”Œ๋ฆฟ ์ตœ์ ํ™” | +| `components/items/ItemMasterDataManagement.tsx` | 1,005 | ํ’ˆ๋ชฉ | ๋„๋ฉ”์ธ ๋กœ์ง ์ถ”์ถœ | + +--- + +## ์ค‘๋ณต ํŒจํ„ด ๋ถ„์„ + +### 1. ์•ก์…˜ ํŒŒ์ผ 80๊ฐœ ๋™์ผ ํŒจํ„ด (~24,000์ค„ ์ค‘๋ณต) + +**ํ˜„์žฌ**: ๋ชจ๋“  ๋„๋ฉ”์ธ์ด ์ด ๊ตฌ์กฐ๋ฅผ ๋ณต๋ถ™ +```typescript +'use server'; +import { serverFetch } from '@/lib/api/fetch-wrapper'; + +interface Api[Domain]Data { ... } // ํƒ€์ž… ์ •์˜ 100~300์ค„ +function transform(data) { ... } // APIโ†’ํ”„๋ก ํŠธ ๋ณ€ํ™˜ 50~100์ค„ + +export async function getList(params) { // ๋ชฉ๋ก ์กฐํšŒ + const url = `${API_URL}/api/v1/endpoint`; + const { response } = await serverFetch(url, { method: 'GET' }); + return transform(response); +} +export async function getById(id) { ... } // ์ƒ์„ธ ์กฐํšŒ +export async function create(data) { ... } // ์ƒ์„ฑ +export async function update(id, data) { ... } // ์ˆ˜์ • +export async function delete(id) { ... } // ์‚ญ์ œ +export async function bulkDelete(ids) { ... } // ์ผ๊ด„ ์‚ญ์ œ +``` + +**ํ•ด๋‹น ๋„๋ฉ”์ธ**: orders, quotes, clients, accounting(13๋ชจ๋“ˆ), hr(6๋ชจ๋“ˆ), production(4๋ชจ๋“ˆ), material(2๋ชจ๋“ˆ), quality(2๋ชจ๋“ˆ), construction(17๋ชจ๋“ˆ), settings(14๋ชจ๋“ˆ) + +**ํ•ด๊ฒฐ ๋ฐฉํ–ฅ**: ์ œ๋„ค๋ฆญ API ์„œ๋น„์Šค ํŒฉํ† ๋ฆฌ +```typescript +// lib/api/createCrudService.ts +function createCrudService(config: { + endpoint: string; + transform: (api: TApi) => TFront; + reverseTransform: (front: TFront) => Partial; +}) { + return { + getList: async (params) => { ... }, + getById: async (id) => { ... }, + create: async (data) => { ... }, + update: async (id, data) => { ... }, + delete: async (id) => { ... }, + bulkDelete: async (ids) => { ... }, + }; +} + +// ์‚ฌ์šฉ: 10์ค„๋กœ ๋ +const orderService = createCrudService({ + endpoint: 'orders', + transform: transformOrder, + reverseTransform: reverseTransformOrder, +}); +``` + +### 2. ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํŒจํ„ด 3๊ฐ€์ง€ ํ˜ผ์žฌ + +| ํŒจํ„ด | ์‚ฌ์šฉ ๋น„์œจ | ์œ„์น˜ | +|------|-----------|------| +| useEffect + .then() ์ง์ ‘ ํ˜ธ์ถœ | ~75% (99+ ์ปดํฌ๋„ŒํŠธ) | ๋Œ€๋ถ€๋ถ„์˜ ๋„๋ฉ”์ธ | +| ์ปค์Šคํ…€ ํ›… (useDetailData ๋“ฑ) | ~15% (~15 ์ปดํฌ๋„ŒํŠธ) | ์‹ ๊ทœ ๊ตฌํ˜„ | +| ApiClient ํด๋ž˜์Šค | ~10% (15 ์ปดํฌ๋„ŒํŠธ) | ๊ฑด์„ค ๋„๋ฉ”์ธ๋งŒ | + +**์ˆ˜๋™ ๋กœ๋”ฉ ์ƒํƒœ ๊ด€๋ฆฌ**: 262๊ณณ์—์„œ ๋ฐ˜๋ณต +```typescript +// ์ด ํŒจํ„ด์ด 262๋ฒˆ ๋ฐ˜๋ณต๋จ +const [data, setData] = useState([]); +const [isLoading, setIsLoading] = useState(true); +const [error, setError] = useState(null); + +useEffect(() => { + setIsLoading(true); + fetchData() + .then(result => setData(result)) + .catch(err => setError(err)) + .finally(() => setIsLoading(false)); +}, []); +``` + +### 3. ํผ ๊ฒ€์ฆ 3๊ฐ€์ง€ ๋ฐฉ์‹ ํ˜ผ์žฌ + +| ๋ฐฉ์‹ | ์‚ฌ์šฉ ํŒŒ์ผ ์ˆ˜ | ๋น„์œจ | +|------|-------------|------| +| Zod ์Šคํ‚ค๋งˆ (์ •์„) | 3๊ฐœ (๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…, ํ’ˆ๋ชฉ) | 5% | +| ์ˆ˜๋™ if๋ฌธ ๊ฒ€์ฆ | 50+๊ฐœ | 60% | +| ๊ฒ€์ฆ ์—†์Œ | ~30๊ฐœ | 35% | + +### 4. ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ํ…œํ”Œ๋ฆฟ ์ด์ค‘ํ™” + +| ๋ฐฉ์‹ | ์‚ฌ์šฉ | ๋น„์œจ | +|------|------|------| +| `UniversalListPage` (์‹ ๊ทœ ํ‘œ์ค€) | 20๊ฐœ ํŽ˜์ด์ง€ | 25% | +| ์ˆ˜๋™ ๊ตฌํ˜„ (๋ ˆ๊ฑฐ์‹œ) | 60+ ํŽ˜์ด์ง€ | 75% | + +### 5. ๋ชจ๋‹ฌ/๋‹ค์ด์–ผ๋กœ๊ทธ 42๊ฐœ ์œ ์‚ฌ ํŒจํ„ด + +**๊ฒ€์ƒ‰/์„ ํƒ ๋ชจ๋‹ฌ 5๊ฐœ+ ๊ฑฐ์˜ ๋™์ผ**: +- `quotes/ItemSearchModal.tsx` +- `production/WorkOrders/AssigneeSelectModal.tsx` +- `material/ReceivingManagement/SupplierSearchModal.tsx` +- `quality/InspectionManagement/OrderSelectModal.tsx` +- `production/WorkOrders/SalesOrderSelectModal.tsx` + +์ „๋ถ€ "๊ฒ€์ƒ‰ ์ž…๋ ฅ โ†’ API ํ˜ธ์ถœ โ†’ ๋ชฉ๋ก ํ‘œ์‹œ โ†’ ์ฒดํฌ๋ฐ•์Šค ์„ ํƒ โ†’ ํ™•์ธ" ๋™์ผ ๊ตฌ์กฐ +โ†’ `SearchableSelectionModal` ํ•˜๋‚˜๋กœ ํ†ตํ•ฉ ๊ฐ€๋Šฅ + +--- + +## ์„ฑ๋Šฅ ์ตœ์ ํ™” ํฌ์ธํŠธ + +| ํ•ญ๋ชฉ | ํ˜„์žฌ ์ƒํƒœ | ์˜ํ–ฅ๋„ | ํ•ด๊ฒฐ ๋ฐฉํ–ฅ | +|------|-----------|--------|-----------| +| React.memo | 551๊ฐœ ์ปดํฌ๋„ŒํŠธ ์ค‘ **1๊ฐœ๋งŒ** ์‚ฌ์šฉ | ๐Ÿ”ด ๋†’์Œ | ๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ/์นด๋“œ ์ปดํฌ๋„ŒํŠธ์— ์ ์šฉ | +| ์ธ๋ผ์ธ ํ™”์‚ดํ‘œ ํ•จ์ˆ˜ | **746๊ณณ** `onClick={() => ...}` | ๐ŸŸก ์ค‘๊ฐ„ | ๋Œ€ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ useCallback ์ ์šฉ | +| useMemo ๋ฏธ์‚ฌ์šฉ | ๋Œ€์šฉ๋Ÿ‰ ๋ฐฐ์—ด ํ•„ํ„ฐ๋ง/์ •๋ ฌ ๊ณณ๊ณณ | ๐ŸŸก ์ค‘๊ฐ„ | ๋น„์šฉ ๋†’์€ ๊ณ„์‚ฐ์— ์ ์šฉ | + +**React.memo ์šฐ์„  ์ ์šฉ ๋Œ€์ƒ** (๋ฆฌ์ŠคํŠธ ๋‚ด ๋ฐ˜๋ณต ๋ Œ๋”๋ง ์ปดํฌ๋„ŒํŠธ): +- `production/WorkerScreen/WorkItemCard.tsx` +- `board/CommentSection/CommentItem.tsx` +- `business/construction/management/ProjectCard.tsx` +- ๊ธฐํƒ€ *Row, *Item, *Card ์ปดํฌ๋„ŒํŠธ 30+๊ฐœ + +--- + +## ํƒ€์ž… ์‹œ์Šคํ…œ ๋ฌธ์ œ + +| ํ•ญ๋ชฉ | ์ˆ˜์น˜ | ์˜ํ–ฅ | +|------|------|------| +| `any` ํƒ€์ž… ์‚ฌ์šฉ | 102๊ณณ (29๊ฐœ ํŒŒ์ผ) | ํƒ€์ž… ์•ˆ์ „์„ฑ ์ €ํ•˜ | +| ๋™์ผ ์—”ํ‹ฐํ‹ฐ ๋‹ค์ค‘ ํƒ€์ž… ์ •์˜ | Vendor, Item, Order ๋“ฑ | ๋ณ€ํ™˜ ์ฝ”๋“œ ~800์ค„ ์ค‘๋ณต | +| types.ts ํŒŒ์ผ | 94๊ฐœ | ์ •๊ทœ ํƒ€์ž… ์ฐพ๊ธฐ ์–ด๋ ค์›€ | +| @ts-ignore/eslint-disable | 25๊ฐœ ํŒŒ์ผ | ์ˆจ๊ฒจ์ง„ ํƒ€์ž… ์—๋Ÿฌ | + +--- + +## ์ถ”์ถœ ๊ฐ€๋Šฅํ•œ ๊ณตํ†ต ํ›… ๋ชฉ๋ก + +### ์ฆ‰์‹œ ์ƒ์„ฑ ๊ฐ€๋Šฅ (ํ”„๋ก ํŠธ ๋‹จ๋…) + +| ํ›… ์ด๋ฆ„ | ๋Œ€์ฒด ๋ฒ”์œ„ | ์˜ˆ์ƒ ์ ˆ๊ฐ | ๊ธฐ์กด ์ฐธ๊ณ  | +|---------|-----------|-----------|-----------| +| `useListData` | 60+ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ | ~4,000์ค„ | hooks/useDetailData.ts ํŒจํ„ด ํ™•์žฅ | +| `useFormSubmit` | 80+ ํผ | ~3,000์ค„ | ์‹ ๊ทœ | +| `usePagination` | 60+ ์ปดํฌ๋„ŒํŠธ | ~1,000์ค„ | ์‹ ๊ทœ | +| `useModal` | 42 ๋ชจ๋‹ฌ | ~500์ค„ | ์‹ ๊ทœ | +| `useClientSideFiltering` | 55+ ์ปดํฌ๋„ŒํŠธ | ~800์ค„ | ์‹ ๊ทœ | + +### ๊ธฐ์กด ํ›… (ํ™œ์šฉ ํ™•๋Œ€ ํ•„์š”) + +| ํ›… | ํ˜„์žฌ ์‚ฌ์šฉ | ์ „์ฒด ์ ์šฉ ์‹œ | +|----|-----------|-------------| +| `useDetailData` | ~15 ์ปดํฌ๋„ŒํŠธ | 100+ ์ƒ์„ธ ํŽ˜์ด์ง€ | +| `useDetailPageState` | ~10 ์ปดํฌ๋„ŒํŠธ | 100+ ์ƒ์„ธ ํŽ˜์ด์ง€ | +| `useCRUDHandlers` | ~10 ์ปดํฌ๋„ŒํŠธ | 80+ CRUD ํŽ˜์ด์ง€ | + +--- + +## ์‹คํ–‰ ๊ณ„ํš + +### Phase 1: ๊ณตํ†ต ํ›… ์ถ”์ถœ (1-2์ฃผ) `ํ”„๋ก ํŠธ ๋‹จ๋…` `ROI ์ตœ๊ณ ` + +> ์ƒˆ ํ›… ์ƒ์„ฑ + ๊ณ ๋นˆ๋„ ํŽ˜์ด์ง€ 20๊ฐœ ์šฐ์„  ์ ์šฉ + +**์ž‘์—… ํ•ญ๋ชฉ**: +``` +- [ ] useListData ํ›… ์ƒ์„ฑ (ํŽ˜์นญ + ํŽ˜์ด์ง€๋„ค์ด์…˜ + ํ•„ํ„ฐ ํ†ตํ•ฉ) +- [ ] useFormSubmit ํ›… ์ƒ์„ฑ (์ œ์ถœ + ๋กœ๋”ฉ + ์—๋Ÿฌ + ํ† ์ŠคํŠธ) +- [ ] usePagination ํ›… ์ƒ์„ฑ +- [ ] useModal ํ›… ์ƒ์„ฑ +- [ ] ๊ณ ๋นˆ๋„ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ 10๊ฐœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + - vendors, clients, items, orders, quotes + - production, quality, material, hr, accounting ๊ฐ 1๊ฐœ +- [ ] ๊ณ ๋นˆ๋„ ํผ ํŽ˜์ด์ง€ 10๊ฐœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + - ์œ„ ๋„๋ฉ”์ธ์˜ ๋“ฑ๋ก/์ˆ˜์ • ํผ ๊ฐ 1๊ฐœ +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: ~8,500์ค„ ์ ˆ๊ฐ, ํŒจํ„ด ์ผ๊ด€์„ฑ +60% + +--- + +### Phase 2: God ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ (2-3์ฃผ) `ํ”„๋ก ํŠธ ๋‹จ๋…` + +> 2000์ค„+ ํŒŒ์ผ 4๊ฐœ + ํ•ต์‹ฌ 1000์ค„+ ํŒŒ์ผ ์šฐ์„  ๋ถ„๋ฆฌ + +**์ž‘์—… ํ•ญ๋ชฉ**: +``` +- [ ] MainDashboard.tsx (2,651์ค„) ๋ถ„๋ฆฌ + โ†’ sections/CEOSection, SalesSection, ProductionSection, QualitySection + โ†’ hooks/useDashboardData + โ†’ utils/calculations +- [ ] ItemMasterContext.tsx (2,406์ค„) ๋ถ„๋ฆฌ + โ†’ ItemContext, SpecificationContext, MaterialContext + โ†’ TemplateContext, AttributeContext +- [ ] useCEODashboard.ts (1,172์ค„) ๋ถ„๋ฆฌ + โ†’ useDailyReport, useReceivables, useMonthlyExpense ๋“ฑ ๊ฐœ๋ณ„ ํ›… + โ†’ ํ›… ํŒฉํ† ๋ฆฌ ํŒจํ„ด ์ ์šฉ +- [ ] lib/api/item-master.ts (2,232์ค„) ๋ถ„๋ฆฌ + โ†’ ๋„๋ฉ”์ธ๋ณ„ API ๋ชจ๋“ˆ (items, specifications, materials, templates) +- [ ] AuthenticatedLayout.tsx (1,289์ค„) + โ†’ useLayoutState, useNavigation, useTenantBranding ํ›… ์ถ”์ถœ +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: ์œ ์ง€๋ณด์ˆ˜์„ฑ +50%, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ ํ™•๋ณด + +--- + +### Phase 3: ์•ก์…˜ ํŒŒ์ผ ์ œ๋„ค๋ฆญํ™” (2-3์ฃผ) `ํ”„๋ก ํŠธ ๋‹จ๋…` `์ตœ๋Œ€ ์ฝ”๋“œ ๊ฐ์†Œ` + +> CRUD ์„œ๋น„์Šค ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ + 80๊ฐœ actions.ts ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +**์ž‘์—… ํ•ญ๋ชฉ**: +``` +- [ ] createCrudService ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜ ๊ตฌํ˜„ + - getList, getById, create, update, delete, bulkDelete + - transform / reverseTransform ์ž๋™ ์ ์šฉ + - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํ‘œ์ค€ํ™” +- [ ] ๊ณตํ†ต transform ์œ ํ‹ธ๋ฆฌํ‹ฐ (lib/transformers/) + - API snake_case โ†’ ํ”„๋ก ํŠธ camelCase ์ž๋™ ๋ณ€ํ™˜ + - ๋‚ ์งœ ๋ฌธ์ž์—ด ํŒŒ์‹ฑ ๊ณตํ†ตํ™” +- [ ] ๊ณ ๋นˆ๋„ ๋„๋ฉ”์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (10๊ฐœ) + - orders, quotes, clients, vendors + - production, quality, material + - hr/employee, hr/vacation, accounting +- [ ] ๋‚˜๋จธ์ง€ 70๊ฐœ ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: ~24,000์ค„ โ†’ ~8,000์ค„ (67% ๊ฐ์†Œ) + +--- + +### Phase 4: ํ…œํ”Œ๋ฆฟ/ํŒจํ„ด ํ†ต์ผ (2-3์ฃผ) `ํ”„๋ก ํŠธ ๋‹จ๋…` + +> UniversalListPage ํ™•๋Œ€ + ๊ฒ€์ฆ ํ‘œ์ค€ํ™” + ๋ชจ๋‹ฌ ํ†ตํ•ฉ + +**์ž‘์—… ํ•ญ๋ชฉ**: +``` +- [ ] UniversalListPage ๊ธฐ๋Šฅ ๋ณด๊ฐ• + - ๊ณ ๊ธ‰ ํ•„ํ„ฐ UI + - ์ปฌ๋Ÿผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• + - ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ +- [ ] ๋ ˆ๊ฑฐ์‹œ ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ โ†’ UniversalListPage ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์šฐ์„  20๊ฐœ) +- [ ] SearchableSelectionModal ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ + - ItemSearchModal, AssigneeSelectModal ๋“ฑ 5๊ฐœ ํ†ตํ•ฉ +- [ ] Zod ๊ฒ€์ฆ ์Šคํ‚ค๋งˆ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ตฌ์ถ• + - lib/validations/common.ts (์ด๋ฉ”์ผ, ์ „ํ™”, ์‚ฌ์—…์ž๋ฒˆํ˜ธ) + - lib/validations/vendor.ts, order.ts, item.ts ๋“ฑ +- [ ] ์ˆ˜๋™ ๊ฒ€์ฆ 50+ ํผ โ†’ Zod ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (์šฐ์„  10๊ฐœ) +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: ~5,000์ค„ ์ ˆ๊ฐ, UX ์ผ๊ด€์„ฑ +80% + +--- + +### Phase 5: ์„ฑ๋Šฅ + ํƒ€์ž… ์ •๋ฆฌ (1-2์ฃผ) `ํ”„๋ก ํŠธ ๋‹จ๋…` + +> React.memo ์ ์šฉ + any ์ œ๊ฑฐ + ํƒ€์ž… ํ†ตํ•ฉ + +**์ž‘์—… ํ•ญ๋ชฉ**: +``` +- [ ] React.memo ์ ์šฉ (๋ฆฌ์ŠคํŠธ ์•„์ดํ…œ ์ปดํฌ๋„ŒํŠธ 30+๊ฐœ) + - WorkItemCard, CommentItem, ProjectCard ๋“ฑ + - *Row, *Item, *Card ํŒจํ„ด ์ „์ˆ˜ ์กฐ์‚ฌ +- [ ] ๋Œ€ํ˜• ์ปดํฌ๋„ŒํŠธ useCallback ์ ์šฉ + - MainDashboard, WorkerScreen, DynamicItemForm +- [ ] any ํƒ€์ž… ์ œ๊ฑฐ (102๊ณณ) + - Phase 1: types/ ํŒŒ์ผ (4๊ฐœ) + - Phase 2: action error handler (50+ ํŒŒ์ผ) + - Phase 3: ์ปดํฌ๋„ŒํŠธ props (20๊ฐœ) +- [ ] ๊ณตํ†ต ํƒ€์ž… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ •๋ฆฌ + - types/shared/ ํด๋” ์ƒ์„ฑ + - ApiResponse, PaginatedResponse, FormState ๋“ฑ + - ์—”ํ‹ฐํ‹ฐ๋ณ„ ์ •๊ทœ ํƒ€์ž… ๋‹จ์ผํ™” +- [ ] @ts-ignore / eslint-disable ์ œ๊ฑฐ (25๊ฐœ ํŒŒ์ผ) +``` + +**์˜ˆ์ƒ ํšจ๊ณผ**: ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง 30-50% ๊ฐœ์„ , ํƒ€์ž… ์•ˆ์ „์„ฑ +60% + +--- + +## ์ „์ฒด ์˜ˆ์ƒ ํšจ๊ณผ ์š”์•ฝ + +| ์ง€ํ‘œ | Phase 1 | Phase 2 | Phase 3 | Phase 4 | Phase 5 | ํ•ฉ๊ณ„ | +|------|---------|---------|---------|---------|---------|------| +| ์ฝ”๋“œ ์ ˆ๊ฐ | ~8,500์ค„ | (๊ตฌ์กฐ ๊ฐœ์„ ) | ~16,000์ค„ | ~5,000์ค„ | (ํ’ˆ์งˆ ๊ฐœ์„ ) | **~29,500์ค„** | +| ํŒจํ„ด ์ผ๊ด€์„ฑ | +60% | +50% | +40% | +80% | +60% | ์ข…ํ•ฉ ๊ฐœ์„  | +| ์œ ์ง€๋ณด์ˆ˜์„ฑ | ๋†’์Œ | ๋งค์šฐ ๋†’์Œ | ๋†’์Œ | ์ค‘๊ฐ„ | ์ค‘๊ฐ„ | ์ข…ํ•ฉ ๊ฐœ์„  | +| ์œ„ํ—˜๋„ | ๋‚ฎ์Œ | ์ค‘๊ฐ„ | ์ค‘๊ฐ„ | ๋‚ฎ์Œ | ๋‚ฎ์Œ | - | + +--- + +## ๋ณ‘๋ ฌ ์ง„ํ–‰ ๊ฐ€๋Šฅ ์กฐํ•ฉ + +``` +[๋…๋ฆฝ ์ง„ํ–‰ ๊ฐ€๋Šฅ] +โ”œโ”€ Phase 1 (๊ณตํ†ต ํ›…) โ”€โ”€โ†’ ์ฆ‰์‹œ ์‹œ์ž‘ +โ”œโ”€ Phase 5 (์„ฑ๋Šฅ/ํƒ€์ž…) โ”€โ†’ ์ฆ‰์‹œ ์‹œ์ž‘ (Phase 1๊ณผ ๋ณ‘๋ ฌ) +โ”‚ +[Phase 1 ์™„๋ฃŒ ํ›„] +โ”œโ”€ Phase 2 (God ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ) โ”€โ”€โ†’ ํ›… ํ™œ์šฉํ•˜์—ฌ ๋ถ„๋ฆฌ +โ”œโ”€ Phase 3 (์•ก์…˜ ์ œ๋„ค๋ฆญํ™”) โ”€โ”€โ”€โ”€โ†’ ๋…๋ฆฝ ์ง„ํ–‰ ๊ฐ€๋Šฅ +โ”‚ +[Phase 1+3 ์™„๋ฃŒ ํ›„] +โ””โ”€ Phase 4 (ํ…œํ”Œ๋ฆฟ ํ†ต์ผ) โ”€โ”€โ”€โ”€โ”€โ†’ ํ›… + ์„œ๋น„์Šค ํ™œ์šฉ +``` + +--- + +## ๊ด€๋ จ ๋ฌธ์„œ + +| ๋ฌธ์„œ | ์„ค๋ช… | +|------|------| +| `[PLAN-2026-02-06] multi-tenancy-optimization-roadmap.md` | ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ณตํ†ตํ™” ๋กœ๋“œ๋งต (๋ณ„๋„ ํŠธ๋ž™) | +| `[ANALYSIS-2026-01-20] ๊ณตํ†ตํ™”-ํ˜„ํ™ฉ-๋ถ„์„.md` | ๊ณตํ†ตํ™” ํ˜„ํ™ฉ ๋ถ„์„ | +| `[ANALYSIS-2026-02-05] list-page-commonization-status.md` | ๋ฆฌ์ŠคํŠธ ํŽ˜์ด์ง€ ๊ณตํ†ตํ™” ํ˜„ํ™ฉ | +| `[IMPL-2026-02-05] detail-hooks-migration-plan.md` | ์ƒ์„ธ ํŽ˜์ด์ง€ ํ›… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš | +| `[IMPL-2026-02-05] formatter-commonization-plan.md` | ํฌ๋งคํ„ฐ ๊ณตํ†ตํ™” ๊ณ„ํš | +| `[PLAN-2026-01-22] ui-component-abstraction.md` | UI ์ปดํฌ๋„ŒํŠธ ์ถ”์ƒํ™” ๊ณ„ํš | +| `guides/[PLAN-2025-12-23] common-component-extraction-plan.md` | ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ ์ถ”์ถœ ๊ณ„ํš | + +--- + +## ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋‚ ์งœ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | +|------|-----------| +| 2026-02-06 | ์ดˆ๊ธฐ ์ž‘์„ฑ - ์ „์ฒด ์ฝ”๋“œ๋ฒ ์ด์Šค ๋ถ„์„ ๊ธฐ๋ฐ˜ 5 Phase ๋กœ๋“œ๋งต | + +--- + +**๋ชจ๋“  Phase ํ”„๋ก ํŠธ ๋‹จ๋… ๊ฐ€๋Šฅ** - ๋ฐฑ์—”๋“œ ์˜์กด์„ฑ ์—†์Œ diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index 000f1964..e73672b7 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -30,6 +30,7 @@ import type { QuoteStatus, ProductCategory, BomCalculationResultItem, + BomCalculationResult, } from './types'; import { transformApiToFrontend, transformFrontendToApi } from './types'; @@ -901,36 +902,8 @@ export interface BomCalculateItem { inspectionFee?: number; } -export interface BomCalculationResult { - success?: boolean; - finished_goods: { - code: string; - name: string; - item_category?: string; - }; - variables?: Record; - items: Array<{ - item_code: string; - item_name: string; - item_category?: string; - specification?: string; - unit?: string; - quantity: number; - quantity_formula?: string; - base_price?: number; - multiplier?: number; - unit_price: number; - total_price: number; - calculation_note?: string; - category_group?: string; - process_group?: string; - process_group_key?: string; - }>; - grouped_items?: Record; - subtotals: Record; - grand_total: number; - debug_steps?: Array<{ step: number; name: string; data: Record }>; // 10๋‹จ๊ณ„ ๊ณ„์‚ฐ ๊ณผ์ • -} +// BomCalculationResult๋Š” types.ts์—์„œ importํ•˜๊ณ  re-export +export type { BomCalculationResult } from './types'; // API ์„œ๋ฒ„ ์‘๋‹ต ๊ตฌ์กฐ (QuoteCalculationService::calculateBomBulk) export interface BomBulkResponse { diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 5ea304ce..6abcd537 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -411,11 +411,17 @@ export interface BomMaterial { export interface BomCalculationResultItem { item_code: string; item_name: string; + item_category?: string; // ํ’ˆ๋ชฉ ์นดํ…Œ๊ณ ๋ฆฌ specification?: string; unit?: string; - quantity: number; // 1๊ฐœ๋‹น BOM ์ˆ˜๋Ÿ‰ (base_quantity) + quantity: number; // 1๊ฐœ๋‹น BOM ์ˆ˜๋Ÿ‰ (base_quantity) + quantity_formula?: string; // ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ ๊ณต์‹ + base_price?: number; // ๊ธฐ๋ณธ ๋‹จ๊ฐ€ + multiplier?: number; // ์Šน์ˆ˜ unit_price: number; total_price: number; + calculation_note?: string; // ๊ณ„์‚ฐ ๋ฉ”๋ชจ + category_group?: string; // ์นดํ…Œ๊ณ ๋ฆฌ ๊ทธ๋ฃน process_group?: string; process_group_key?: string; // Legacy ๊ณต์ • ๊ทธ๋ฃน ํ‚ค category_code?: string; // ์•„์ดํ…œ ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ (๋™์  ์นดํ…Œ๊ณ ๋ฆฌ ์‹œ์Šคํ…œ) @@ -431,6 +437,7 @@ export interface BomDebugStep { // BOM ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ํƒ€์ž… export interface BomCalculationResult { + success?: boolean; // API ์‘๋‹ต ์‹œ ์„ฑ๊ณต ์—ฌ๋ถ€ finished_goods: { code: string; name: string;