From 8b7e31031dfccded90359f6b93b6715d42c96d34 Mon Sep 17 00:00:00 2001 From: syc0123 Date: Thu, 12 Mar 2026 10:12:56 +0900 Subject: [PATCH 01/18] refactor: Improve numbering rule service for manual prefix handling and sequence allocation - Modified the `buildPrefixKey` function to include an optional `manualValues` parameter, allowing manual input values to be incorporated into the prefix key. - Adjusted the sequence allocation process in `allocateCode` to extract manual values before building the prefix key, ensuring accurate prefix generation. - Removed the fallback to the "BULK1" value in manual configurations, preventing unintended overwrites and ensuring user input is prioritized. - Enhanced the `joinPartsWithSeparators` function to prevent consecutive separators when handling empty parts, improving the output format. - Added a new migration script to clean up existing "BULK1" values from the database, ensuring data integrity. These changes address several issues related to manual input handling and improve the overall functionality of the numbering rule service. --- .../src/services/numberingRuleService.ts | 287 ++++++++------ .../MPN[계획]-품번-수동접두어채번.md | 369 ++++++++++++++++++ .../MPN[맥락]-품번-수동접두어채번.md | 130 ++++++ .../MPN[체크]-품번-수동접두어채번.md | 79 ++++ 4 files changed, 736 insertions(+), 129 deletions(-) create mode 100644 docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md create mode 100644 docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md create mode 100644 docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 91ae4cb5..f4175b9d 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -39,7 +39,9 @@ function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globa result += val; if (idx < partValues.length - 1) { const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator; - result += sep; + if (val || !result.endsWith(sep)) { + result += sep; + } } }); return result; @@ -74,16 +76,22 @@ class NumberingRuleService { */ private async buildPrefixKey( rule: NumberingRuleConfig, - formData?: Record + formData?: Record, + manualValues?: string[] ): Promise { const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order); const prefixParts: string[] = []; + let manualIndex = 0; for (const part of sortedParts) { if (part.partType === "sequence") continue; if (part.generationMethod === "manual") { - // 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로) + const manualValue = manualValues?.[manualIndex] || ""; + manualIndex++; + if (manualValue) { + prefixParts.push(manualValue); + } continue; } @@ -1302,11 +1310,29 @@ class NumberingRuleService { const rule = await this.getRuleById(ruleId, companyCode); if (!rule) throw new Error("규칙을 찾을 수 없습니다"); - // prefix_key 기반 순번: 순번 이외 파트 조합으로 prefix 생성 - const prefixKey = await this.buildPrefixKey(rule, formData); + // 1단계: 수동 값 추출 (buildPrefixKey 전에 수행해야 prefix_key에 포함 가능) + const manualParts = rule.parts.filter( + (p: any) => p.generationMethod === "manual" + ); + let extractedManualValues: string[] = []; + + if (manualParts.length > 0 && userInputCode) { + extractedManualValues = await this.extractManualValuesFromInput( + rule, userInputCode, formData + ); + + // 템플릿 파싱 실패 시 userInputCode 전체를 수동 값으로 사용 (수동 파트 1개인 경우만) + if (extractedManualValues.length === 0 && manualParts.length === 1) { + extractedManualValues = [userInputCode]; + logger.info("수동 값 추출 폴백: userInputCode 전체 사용", { userInputCode }); + } + } + + // 2단계: prefix_key 빌드 (수동 값 포함) + const prefixKey = await this.buildPrefixKey(rule, formData, extractedManualValues); const hasSequence = rule.parts.some((p: any) => p.partType === "sequence"); - // 순번이 있으면 prefix_key 기반으로 UPSERT하여 다음 순번 획득 + // 3단계: 순번이 있으면 prefix_key 기반으로 UPSERT하여 다음 순번 획득 let allocatedSequence = 0; if (hasSequence) { allocatedSequence = await this.incrementSequenceForPrefix( @@ -1320,136 +1346,15 @@ class NumberingRuleService { } logger.info("allocateCode: prefix_key 기반 순번 할당", { - ruleId, prefixKey, allocatedSequence, + ruleId, prefixKey, allocatedSequence, extractedManualValues, }); - // 수동 입력 파트가 있고, 사용자가 입력한 코드가 있으면 수동 입력 부분 추출 - const manualParts = rule.parts.filter( - (p: any) => p.generationMethod === "manual" - ); - let extractedManualValues: string[] = []; - - if (manualParts.length > 0 && userInputCode) { - const previewParts = await Promise.all(rule.parts - .sort((a: any, b: any) => a.order - b.order) - .map(async (part: any) => { - if (part.generationMethod === "manual") { - return "____"; - } - const autoConfig = part.autoConfig || {}; - switch (part.partType) { - case "sequence": { - const length = autoConfig.sequenceLength || 3; - return "X".repeat(length); - } - case "text": - return autoConfig.textValue || ""; - case "date": - return "DATEPART"; - case "category": { - const catKey2 = autoConfig.categoryKey; - const catMappings2 = autoConfig.categoryMappings || []; - - if (!catKey2 || !formData) { - return "CATEGORY"; - } - - const colName2 = catKey2.includes(".") - ? catKey2.split(".")[1] - : catKey2; - const selVal2 = formData[colName2]; - - if (!selVal2) { - return "CATEGORY"; - } - - const selValStr2 = String(selVal2); - let catMapping2 = catMappings2.find((m: any) => { - if (m.categoryValueId?.toString() === selValStr2) return true; - if (m.categoryValueCode && m.categoryValueCode === selValStr2) return true; - if (m.categoryValueLabel === selValStr2) return true; - return false; - }); - - if (!catMapping2) { - try { - const pool2 = getPool(); - const [ct2, cc2] = catKey2.includes(".") ? catKey2.split(".") : [catKey2, catKey2]; - const cvr2 = await pool2.query( - `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, - [ct2, cc2, selValStr2] - ); - if (cvr2.rows.length > 0) { - const rid2 = cvr2.rows[0].value_id; - const rlabel2 = cvr2.rows[0].value_label; - catMapping2 = catMappings2.find((m: any) => { - if (m.categoryValueId?.toString() === String(rid2)) return true; - if (m.categoryValueLabel === rlabel2) return true; - return false; - }); - } - } catch { /* ignore */ } - } - - return catMapping2?.format || "CATEGORY"; - } - case "reference": { - const refCol2 = autoConfig.referenceColumnName; - if (refCol2 && formData && formData[refCol2]) { - return String(formData[refCol2]); - } - return "REF"; - } - default: - return ""; - } - })); - - const sortedPartsForTemplate = rule.parts.sort((a: any, b: any) => a.order - b.order); - const previewTemplate = joinPartsWithSeparators(previewParts, sortedPartsForTemplate, rule.separator || ""); - - const templateParts = previewTemplate.split("____"); - if (templateParts.length > 1) { - let remainingCode = userInputCode; - for (let i = 0; i < templateParts.length - 1; i++) { - const prefix = templateParts[i]; - const suffix = templateParts[i + 1]; - - if (prefix && remainingCode.startsWith(prefix)) { - remainingCode = remainingCode.slice(prefix.length); - } - - if (suffix) { - const suffixStart = suffix.replace(/X+|DATEPART/g, ""); - const manualEndIndex = suffixStart - ? remainingCode.indexOf(suffixStart) - : remainingCode.length; - if (manualEndIndex > 0) { - extractedManualValues.push( - remainingCode.slice(0, manualEndIndex) - ); - remainingCode = remainingCode.slice(manualEndIndex); - } - } else { - extractedManualValues.push(remainingCode); - } - } - } - - logger.info( - `수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}` - ); - } - let manualPartIndex = 0; const parts = await Promise.all(rule.parts .sort((a: any, b: any) => a.order - b.order) .map(async (part: any) => { if (part.generationMethod === "manual") { - const manualValue = - extractedManualValues[manualPartIndex] || - part.manualConfig?.value || - ""; + const manualValue = extractedManualValues[manualPartIndex] || ""; manualPartIndex++; return manualValue; } @@ -1593,6 +1498,130 @@ class NumberingRuleService { return this.allocateCode(ruleId, companyCode); } + /** + * 사용자 입력 코드에서 수동 파트 값을 추출 + * 템플릿 기반 파싱으로 수동 입력 위치("____")에 해당하는 값을 분리 + */ + private async extractManualValuesFromInput( + rule: NumberingRuleConfig, + userInputCode: string, + formData?: Record + ): Promise { + const extractedValues: string[] = []; + + const previewParts = await Promise.all(rule.parts + .sort((a: any, b: any) => a.order - b.order) + .map(async (part: any) => { + if (part.generationMethod === "manual") { + return "____"; + } + const autoConfig = part.autoConfig || {}; + switch (part.partType) { + case "sequence": { + const length = autoConfig.sequenceLength || 3; + return "X".repeat(length); + } + case "text": + return autoConfig.textValue || ""; + case "date": + return "DATEPART"; + case "category": { + const catKey2 = autoConfig.categoryKey; + const catMappings2 = autoConfig.categoryMappings || []; + + if (!catKey2 || !formData) { + return ""; + } + + const colName2 = catKey2.includes(".") + ? catKey2.split(".")[1] + : catKey2; + const selVal2 = formData[colName2]; + + if (!selVal2) { + return ""; + } + + const selValStr2 = String(selVal2); + let catMapping2 = catMappings2.find((m: any) => { + if (m.categoryValueId?.toString() === selValStr2) return true; + if (m.categoryValueCode && m.categoryValueCode === selValStr2) return true; + if (m.categoryValueLabel === selValStr2) return true; + return false; + }); + + if (!catMapping2) { + try { + const pool2 = getPool(); + const [ct2, cc2] = catKey2.includes(".") ? catKey2.split(".") : [catKey2, catKey2]; + const cvr2 = await pool2.query( + `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, + [ct2, cc2, selValStr2] + ); + if (cvr2.rows.length > 0) { + const rid2 = cvr2.rows[0].value_id; + const rlabel2 = cvr2.rows[0].value_label; + catMapping2 = catMappings2.find((m: any) => { + if (m.categoryValueId?.toString() === String(rid2)) return true; + if (m.categoryValueLabel === rlabel2) return true; + return false; + }); + } + } catch { /* ignore */ } + } + + return catMapping2?.format || ""; + } + case "reference": { + const refCol2 = autoConfig.referenceColumnName; + if (refCol2 && formData && formData[refCol2]) { + return String(formData[refCol2]); + } + return ""; + } + default: + return ""; + } + })); + + const sortedPartsForTemplate = rule.parts.sort((a: any, b: any) => a.order - b.order); + const previewTemplate = joinPartsWithSeparators(previewParts, sortedPartsForTemplate, rule.separator || ""); + + const templateParts = previewTemplate.split("____"); + if (templateParts.length > 1) { + let remainingCode = userInputCode; + for (let i = 0; i < templateParts.length - 1; i++) { + const prefix = templateParts[i]; + const suffix = templateParts[i + 1]; + + if (prefix && remainingCode.startsWith(prefix)) { + remainingCode = remainingCode.slice(prefix.length); + } + + if (suffix) { + const suffixStart = suffix.replace(/X+|DATEPART/g, ""); + const manualEndIndex = suffixStart + ? remainingCode.indexOf(suffixStart) + : remainingCode.length; + if (manualEndIndex > 0) { + extractedValues.push( + remainingCode.slice(0, manualEndIndex) + ); + remainingCode = remainingCode.slice(manualEndIndex); + } + } else { + extractedValues.push(remainingCode); + } + } + } + + logger.info( + `수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedValues)}` + ); + + return extractedValues; + } + private formatDate(date: Date, format: string): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); diff --git a/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md new file mode 100644 index 00000000..b3337f9e --- /dev/null +++ b/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md @@ -0,0 +1,369 @@ +# [계획서] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성 + +> 관련 문서: [맥락노트](./MPN[맥락]-품번-수동접두어채번.md) | [체크리스트](./MPN[체크]-품번-수동접두어채번.md) + +## 개요 + +기준정보 - 품목 정보 등록 모달에서 품번(`item_number`) 채번의 세 가지 문제를 해결합니다. + +1. **BULK1 덮어쓰기 문제**: 사용자가 "ㅁㅁㅁ"을 입력해도 수동 값 추출이 실패하여 DB 숨은 값 `manualConfig.value = "BULK1"`로 덮어씌워짐 +2. **순번 공유 문제**: `buildPrefixKey`가 수동 파트를 건너뛰어 모든 접두어가 같은 시퀀스 카운터를 공유함 +3. **연속 구분자(--) 문제**: 카테고리가 비었을 때 `joinPartsWithSeparators`가 빈 파트에도 구분자를 붙여 `--` 발생 + 템플릿 불일치로 수동 값 추출 실패 → `userInputCode` 전체(구분자 포함)가 수동 값이 됨 + +--- + +## 현재 동작 + +### 채번 규칙 구성 (옵션설정 > 코드설정) + +``` +규칙1(카테고리/재질, 자동) → "-" → 규칙2(문자, 직접입력) → "-" → 규칙3(순번, 자동, 3자리, 시작=5) +``` + +### 실제 저장 흐름 (사용자가 "ㅁㅁㅁ" 입력 시) + +1. 모달 열림 → `_numberingRuleId` 설정됨 (TextInputComponent L117-128) +2. 사용자가 "ㅁㅁㅁ" 입력 → `formData.item_number = "ㅁㅁㅁ"` +3. 저장 클릭 → `buttonActions.ts`가 `_numberingRuleId` 확인 → `allocateCode(ruleId, "ㅁㅁㅁ", formData)` 호출 +4. 백엔드: 템플릿 기반 수동 값 추출 시도 → **실패** (입력 "ㅁㅁㅁ"이 템플릿 "CATEGORY-____-XXX"와 불일치) +5. 폴백: `manualConfig.value = "BULK1"` 사용 → **사용자 입력 "ㅁㅁㅁ" 완전 무시됨** +6. `buildPrefixKey`가 수동 파트를 건너뜀 → prefix_key에 접두어 미포함 → 공유 카운터 사용 +7. 결과: **-BULK1-015** (사용자가 뭘 입력하든 항상 BULK1, 항상 공유 카운터) + +### 문제 1: 순번 공유 (buildPrefixKey) + +**위치**: `numberingRuleService.ts` L85-88 + +```typescript +if (part.generationMethod === "manual") { + // 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로) + continue; // ← 접두어별 순번 분리를 막는 원인 +} +``` + +이 `continue` 때문에 수동 입력값이 prefix_key에 포함되지 않습니다. +"ㅁㅁㅁ", "ㅇㅇㅇ", "BULK1" 전부 **같은 시퀀스 카운터를 공유**합니다. + +### 문제 2: BULK1 덮어쓰기 (추출 실패 + manualConfig.value 폴백) + +**발생 흐름**: + +1. 사용자가 "ㅁㅁㅁ" 입력 → `userInputCode = "ㅁㅁㅁ"` 으로 `allocateCode` 호출 +2. `allocateCode` 내부에서 **prefix_key를 먼저 빌드** (L1306) → 수동 값 추출은 그 이후 (L1332-1442) +3. 템플릿 기반 수동 값 추출 시도 (L1411-1436): + ``` + 템플릿: "카테고리값-____-XXX" (카테고리값-수동입력위치-순번) + 사용자 입력: "ㅁㅁㅁ" + ``` +4. "ㅁㅁㅁ"은 "카테고리값-"으로 시작하지 않음 → `startsWith` 불일치 → **추출 실패** → `extractedManualValues = []` +5. 코드 조합 단계 (L1448-1454)에서 폴백 체인 동작: + ```typescript + const manualValue = + extractedManualValues[0] || // undefined (추출 실패) + part.manualConfig?.value || // "BULK1" (DB 숨은 값) ← 여기서 덮어씌워짐 + ""; + ``` +6. 결과: `-BULK1-015` (사용자 입력 "ㅁㅁㅁ"이 완전히 무시됨) + +**DB 숨은 값 원인**: +- DB `numbering_rule_parts.manual_config` 컬럼에 `{"value": "BULK1", "placeholder": "..."}` 저장됨 +- `ManualConfigPanel.tsx`에는 `placeholder` 입력란만 있고 **`value` 입력란이 없음** +- 플레이스홀더 수정 시 `{ ...config, placeholder: ... }` 스프레드로 기존 `value: "BULK1"`이 계속 보존됨 + +### 문제 3: 연속 구분자(--) 문제 + +**발생 흐름**: + +1. 카테고리 미선택 → 카테고리 파트 값 = `""` (빈 문자열) +2. `joinPartsWithSeparators`가 빈 파트에도 구분자 `-`를 추가 → 연속 빈 파트 시 `--` 발생 +3. 사용자 입력 필드에 `-제발-015` 형태로 표시 (선행 `-`) +4. `extractManualValuesFromInput`에서 템플릿이 `CATEGORY-____-XXX`로 생성됨 (실제 값 `""` 대신 플레이스홀더 `"CATEGORY"` 사용) +5. 입력 `-제발-015`이 `CATEGORY-`로 시작하지 않음 → 추출 실패 +6. 폴백: `userInputCode` 전체 `-제발-015`가 수동 값이 됨 +7. 코드 조합: `""` + `-` + `-제발-015` + `-` + `003` = `--제발-015-003` + +### 정상 동작 확인된 부분 + +| 항목 | 상태 | 근거 | +|------|------|------| +| `_numberingRuleId` 유지 | 정상 | 사용자 입력해도 allocateCode가 호출됨 | +| 시퀀스 증가 | 정상 | 순번이 증가하고 있음 (015 등) | +| 코드 조합 | 정상 | 구분자, 파트 순서 등 올바르게 결합됨 | + +### 비정상 확인된 부분 + +| 항목 | 상태 | 근거 | +|------|------|------| +| 수동 값 추출 | **실패** | 사용자 입력 "ㅁㅁㅁ"이 템플릿과 불일치 → 추출 실패 → BULK1 폴백 | +| prefix_key 분리 | **실패** | `buildPrefixKey`가 수동 파트 skip → 모든 접두어가 같은 시퀀스 공유 | +| 연속 구분자 | **실패** | 빈 파트에 구분자 추가 + 템플릿 플레이스홀더 불일치 → `--` 발생 | + +--- + +## 변경 후 동작 + +### prefix_key에 수동 파트 값 포함 + +``` +현재: prefix_key = 카테고리값만 (수동 파트 무시) +변경: prefix_key = 카테고리값 + "|" + 수동입력값 +``` + +### allocateCode 실행 순서 변경 + +``` +현재: buildPrefixKey → 시퀀스 할당 → 수동 값 추출 → 코드 조합 +변경: 수동 값 추출 → buildPrefixKey(수동 값 포함) → 시퀀스 할당 → 코드 조합 +``` + +### 순번 동작 + +``` +"ㅁㅁㅁ" 첫 등록 → prefix_key="카테고리|ㅁㅁㅁ", sequence=1 → -ㅁㅁㅁ-001 +"ㅁㅁㅁ" 두번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=2 → -ㅁㅁㅁ-002 +"ㅇㅇㅇ" 첫 등록 → prefix_key="카테고리|ㅇㅇㅇ", sequence=1 → -ㅇㅇㅇ-001 +"ㅁㅁㅁ" 세번째 → prefix_key="카테고리|ㅁㅁㅁ", sequence=3 → -ㅁㅁㅁ-003 +``` + +### BULK1 폴백 제거 (코드 + DB 이중 조치) + +``` +코드: 폴백 체인에서 manualConfig.value 제거 → extractedManualValues만 사용 +DB: manual_config에서 "value": "BULK1" 키 제거 → 유령 기본값 정리 +``` + +### 연속 구분자 방지 + 템플릿 정합성 복원 + +``` +joinPartsWithSeparators: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음 +extractManualValuesFromInput: 카테고리/참조 빈 값 시 "" 반환 (플레이스홀더 "CATEGORY"/"REF" 대신) +→ 템플릿이 실제 코드 구조와 일치 → 추출 성공 → -- 방지 +``` + +--- + +## 시각적 예시 + +| 사용자 입력 | 현재 동작 | 원인 | 변경 후 동작 | +|------------|----------|------|-------------| +| `ㅁㅁㅁ` (첫번째) | `-BULK1-015` | 추출 실패 → BULK1 폴백 + 공유 카운터 | `카테고리값-ㅁㅁㅁ-001` | +| `ㅁㅁㅁ` (두번째) | `-BULK1-016` | 동일 | `카테고리값-ㅁㅁㅁ-002` | +| `ㅇㅇㅇ` (첫번째) | `-BULK1-017` | 동일 | `카테고리값-ㅇㅇㅇ-001` | +| (입력 안 함) | `-BULK1-018` | manualConfig.value 폴백 | 에러 반환 (수동 파트 필수 입력) | +| 카테고리 비었을 때 | `--제발-015-003` | 빈 파트 구분자 중복 + 템플릿 불일치 | `-제발-001` | + +--- + +## 아키텍처 + +```mermaid +sequenceDiagram + participant User as 사용자 + participant BA as buttonActions.ts + participant API as allocateNumberingCode API + participant NRS as numberingRuleService + participant DB as numbering_rule_sequences + + User->>BA: 저장 클릭 (item_number = "ㅁㅁㅁ") + BA->>API: allocateCode(ruleId, "ㅁㅁㅁ", formData) + API->>NRS: allocateCode() + + Note over NRS: 1단계: 수동 값 추출 (buildPrefixKey 전에 수행) + NRS->>NRS: extractManualValuesFromInput("ㅁㅁㅁ") + Note over NRS: 템플릿 파싱 실패 → 폴백: userInputCode 전체 사용 + NRS->>NRS: extractedManualValues = ["ㅁㅁㅁ"] + + Note over NRS: 2단계: prefix_key 빌드 (수동 값 포함) + NRS->>NRS: buildPrefixKey(rule, formData, ["ㅁㅁㅁ"]) + Note over NRS: prefix_key = "카테고리값|ㅁㅁㅁ" + + Note over NRS: 3단계: 시퀀스 할당 + NRS->>DB: UPSERT sequences (prefix_key="카테고리값|ㅁㅁㅁ") + DB-->>NRS: current_sequence = 1 + + Note over NRS: 4단계: 코드 조합 + NRS->>NRS: 카테고리값 + "-" + "ㅁㅁㅁ" + "-" + "001" + NRS-->>API: "카테고리값-ㅁㅁㅁ-001" + API-->>BA: generatedCode + BA->>BA: formData.item_number = "카테고리값-ㅁㅁㅁ-001" +``` + +--- + +## 변경 대상 파일 + +| 파일 | 변경 내용 | 규모 | +|------|----------|------| +| `backend-node/src/services/numberingRuleService.ts` | `buildPrefixKey`에 `manualValues` 파라미터 추가, `allocateCode`에서 수동 값 추출 순서 변경 + 폴백 체인 정리, `extractManualValuesFromInput` 헬퍼 분리, `joinPartsWithSeparators` 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경 | ~60줄 | +| `db/migrations/1053_remove_bulk1_manual_config_value.sql` | `numbering_rule_parts.manual_config`에서 `value: "BULK1"` 제거 | SQL 1건 | + +> `TextInputComponent.tsx` 변경 불필요. `_numberingRuleId`가 유지되고 있으며, 수동 값 추출도 정상 동작 확인됨. +> 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요. + +### buildPrefixKey 호출부 영향 분석 + +| 호출부 | 위치 | `manualValues` 전달 | 영향 | +|--------|------|---------------------|------| +| `previewCode` | L1091 | 미전달 (undefined) | 변화 없음 | +| `allocateCode` | L1332 | 전달 | prefix_key에 수동 값 포함됨 | + +### 멀티테넌시 체크 + +| 항목 | 상태 | 근거 | +|------|------|------| +| `buildPrefixKey` | 영향 없음 | 시그니처만 확장, company_code 관련 변경 없음 | +| `allocateCode` | 이미 준수 | L1302에서 `companyCode`로 규칙 조회, L1313에서 시퀀스 할당 시 `companyCode` 전달 | +| `joinPartsWithSeparators` | 영향 없음 | 순수 문자열 조합 함수, company_code 무관 | +| DB 마이그레이션 | 해당 없음 | JSONB 내부 값 정리, company_code 무관 | + +--- + +## 코드 설계 + +### 1. `joinPartsWithSeparators` 수정 - 연속 구분자 방지 + +**위치**: L36-48 +**변경**: 빈 파트 뒤에 이미 구분자가 있으면 중복 추가하지 않음 + +```typescript +function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globalSeparator: string): string { + let result = ""; + partValues.forEach((val, idx) => { + result += val; + if (idx < partValues.length - 1) { + const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator; + if (val || !result.endsWith(sep)) { + result += sep; + } + } + }); + return result; +} +``` + +### 2. `buildPrefixKey` 수정 - 수동 파트 값을 prefix에 포함 + +**위치**: L75-88 +**변경**: 세 번째 파라미터 `manualValues` 추가. 전달되면 prefix_key에 포함. + +```typescript +private async buildPrefixKey( + rule: NumberingRuleConfig, + formData?: Record, + manualValues?: string[] +): Promise { + const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order); + const prefixParts: string[] = []; + let manualIndex = 0; + + for (const part of sortedParts) { + if (part.partType === "sequence") continue; + + if (part.generationMethod === "manual") { + const manualValue = manualValues?.[manualIndex] || ""; + manualIndex++; + if (manualValue) { + prefixParts.push(manualValue); + } + continue; + } + + // ... 나머지 기존 로직 (text, date, category, reference 등) 그대로 유지 ... + } + + return prefixParts.join("|"); +} +``` + +**하위 호환성**: `manualValues`는 optional. `previewCode`(L1091)는 전달하지 않으므로 동작 변화 없음. + +### 3. `allocateCode` 수정 - 수동 값 추출 순서 변경 + 폴백 정리 + +**위치**: L1290-1584 +**핵심 변경 2가지**: + +(A) 기존에는 `buildPrefixKey`(L1306) → 수동 값 추출(L1332) 순서였으나, **수동 값 추출 → `buildPrefixKey`** 순서로 변경. + +(B) 코드 조합 단계(L1448-1454)에서 `manualConfig.value` 폴백 제거. + +```typescript +async allocateCode(ruleId, companyCode, formData?, userInputCode?) { + // ... 규칙 조회 ... + + // 1단계: 수동 파트 값 추출 (buildPrefixKey 호출 전에 수행) + const manualParts = rule.parts.filter(p => p.generationMethod === "manual"); + let extractedManualValues: string[] = []; + + if (manualParts.length > 0 && userInputCode) { + extractedManualValues = await this.extractManualValuesFromInput( + rule, userInputCode, formData + ); + + // 폴백: 추출 실패 시 userInputCode 전체를 수동 값으로 사용 + if (extractedManualValues.length === 0 && manualParts.length === 1) { + extractedManualValues = [userInputCode]; + } + } + + // 2단계: 수동 값을 포함하여 prefix_key 빌드 + const prefixKey = await this.buildPrefixKey(rule, formData, extractedManualValues); + + // 3단계: 시퀀스 할당 (기존 로직 그대로) + + // 4단계: 코드 조합 (manualConfig.value 폴백 제거) + // 기존: extractedManualValues[i] || part.manualConfig?.value || "" + // 변경: extractedManualValues[i] || "" +} +``` + +### 4. `extractManualValuesFromInput` 헬퍼 분리 + 템플릿 정합성 복원 + +기존 `allocateCode` 내부의 수동 값 추출 로직(L1332-1442)을 별도 private 메서드로 추출. +로직 자체는 변경 없음, 위치만 이동. +카테고리/참조 파트의 빈 값 처리를 실제 코드 생성과 일치시킴. + +```typescript +private async extractManualValuesFromInput( + rule: NumberingRuleConfig, + userInputCode: string, + formData?: Record +): Promise { + // 기존 L1332-1442의 로직을 그대로 이동 + // 변경: 카테고리/참조 빈 값 시 "CATEGORY"/"REF" 대신 "" 반환 + // → 템플릿이 실제 코드 구조와 일치 → 추출 성공률 향상 +} +``` + +### 5. DB 마이그레이션 - BULK1 유령 기본값 제거 + +**파일**: `db/migrations/1053_remove_bulk1_manual_config_value.sql` + +`numbering_rule_parts.manual_config` 컬럼에서 `value` 키를 제거합니다. + +```sql +-- manual_config에서 "value" 키 제거 (BULK1 유령 기본값 정리) +UPDATE numbering_rule_parts +SET manual_config = manual_config - 'value' +WHERE generation_method = 'manual' + AND manual_config ? 'value' + AND manual_config->>'value' = 'BULK1'; +``` + +> PostgreSQL JSONB 연산자 `-`를 사용하여 특정 키만 제거. +> `manual_config`의 나머지 필드(`placeholder` 등)는 유지됨. +> "BULK1" 값을 가진 레코드만 대상으로 하여 안전성 확보. + +--- + +## 설계 원칙 + +- **변경 범위 최소화**: `numberingRuleService.ts` 코드 변경 + DB 마이그레이션 1건 +- **이중 조치**: 코드에서 `manualConfig.value` 폴백 제거 + DB에서 유령 값 정리 +- `buildPrefixKey`의 `manualValues`는 optional → 기존 호출부(`previewCode` 등)에 영향 없음 +- `allocateCode` 내부 로직 순서만 변경 (추출 → prefix_key 빌드), 새 로직 추가 아님 +- 수동 값 추출 로직은 기존 코드를 헬퍼로 분리할 뿐, 로직 자체는 변경 없음 +- DB 마이그레이션은 "BULK1" 값만 정확히 타겟팅하여 부작용 방지 +- `TextInputComponent.tsx` 변경 불필요 (현재 동작이 올바름) +- 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요 +- `joinPartsWithSeparators`는 연속 구분자만 방지, 기존 구분자 구조 유지 +- 템플릿 카테고리/참조 빈 값을 실제 코드와 일치시켜 추출 성공률 향상 diff --git a/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md new file mode 100644 index 00000000..9ff76513 --- /dev/null +++ b/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md @@ -0,0 +1,130 @@ +# [맥락노트] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성 + +> 관련 문서: [계획서](./MPN[계획]-품번-수동접두어채번.md) | [체크리스트](./MPN[체크]-품번-수동접두어채번.md) + +--- + +## 왜 이 작업을 하는가 + +- 기준정보 - 품목정보 등록 모달에서 품번 인풋에 사용자가 값을 입력해도 무시되고 "BULK1"로 저장됨 +- 서로 다른 접두어("ㅁㅁㅁ", "ㅇㅇㅇ")를 입력해도 전부 같은 시퀀스 카운터를 공유함 +- 카테고리 미선택 시 `--제발-015-003` 처럼 연속 구분자가 발생함 +- 사용자 입력이 반영되고, 접두어별로 독립된 순번이 부여되어야 함 + +--- + +## 핵심 결정 사항과 근거 + +### 1. 수동 값 추출을 buildPrefixKey 전으로 이동 + +- **결정**: `allocateCode` 내부에서 수동 값 추출 → buildPrefixKey 순서로 변경 +- **근거**: 기존에는 buildPrefixKey(L1306)가 먼저 실행된 후 수동 값 추출(L1332)이 진행됨. 수동 값이 prefix_key에 포함되려면 추출이 먼저 되어야 함 +- **대안 검토**: buildPrefixKey 내부에서 직접 추출 → 기각 (역할 분리 위반, previewCode 호출에도 영향) + +### 2. buildPrefixKey에 수동 파트 값 포함 + +- **결정**: `manualValues` optional 파라미터 추가, 전달되면 prefix_key에 포함 +- **근거**: 기존 `continue`(L85-87)로 수동 파트가 prefix_key에서 제외되어 모든 접두어가 같은 시퀀스를 공유함 +- **하위호환**: optional 파라미터이므로 `previewCode`(L1091) 등 기존 호출부는 영향 없음 + +### 3. 템플릿 파싱 실패 시 userInputCode 전체를 수동 값으로 사용 + +- **결정**: 수동 파트가 1개이고 템플릿 기반 추출이 실패하면 `userInputCode` 전체를 수동 값으로 사용 +- **근거**: 사용자가 "ㅁㅁㅁ"처럼 접두어 부분만 입력하면 템플릿 "카테고리값-____-XXX"와 불일치. `startsWith` 조건 실패로 추출이 안 됨. 이 경우 입력 전체가 수동 값임 +- **제한**: 수동 파트가 2개 이상이면 이 폴백 불가 (어디서 분리할지 알 수 없음) + +### 4. 코드 조합에서 manualConfig.value 폴백 제거 + +- **결정**: `extractedManualValues[i] || part.manualConfig?.value || ""` → `extractedManualValues[i] || ""` +- **근거**: `manualConfig.value`는 UI에서 입력/편집할 수 없는 유령 필드. `ManualConfigPanel.tsx`에 `value` 입력란이 없어 DB에 한번 저장되면 스프레드 연산자로 계속 보존됨 +- **이중 조치**: 코드에서 폴백 제거 + DB 마이그레이션으로 기존 "BULK1" 값 정리 + +### 5. DB 마이그레이션은 BULK1만 타겟팅 + +- **결정**: `manual_config->>'value' = 'BULK1'` 조건으로 한정 +- **근거**: 다른 value가 의도적으로 설정된 경우가 있을 수 있음. 확인된 문제("BULK1")만 정리하여 부작용 방지 +- **대안 검토**: 전체 `manual_config.value` 키 제거 → 보류 (운영 판단 필요) + +### 6. extractManualValuesFromInput 헬퍼 분리 + +- **결정**: 기존 `allocateCode` 내부의 수동 값 추출 로직(L1332-1442)을 별도 private 메서드로 추출 +- **근거**: 추출 로직이 약 110줄로 `allocateCode`가 과도하게 비대함. 헬퍼로 분리하면 순서 변경도 자연스러움 +- **원칙**: 로직 자체는 변경 없음, 위치만 이동 (구조적 변경과 행위적 변경 분리) + +### 7. 프론트엔드 변경 불필요 + +- **결정**: 프론트엔드 코드 수정 없음 +- **근거**: `_numberingRuleId`가 사용자 입력 시에도 유지되고 있음 확인. `buttonActions.ts`가 정상적으로 `allocateCode`를 호출함. 문제는 백엔드 로직에만 있음 + +### 8. joinPartsWithSeparators 연속 구분자 방지 + +- **결정**: 빈 파트 뒤에 이미 같은 구분자가 있으면 중복 추가하지 않음 +- **근거**: 카테고리가 비면 파트 값 `""` + 구분자 `-`가 반복되어 `--` 발생. 구분자 구조(`-ㅁㅁㅁ-001`)는 유지하되 연속(`--`)만 방지 +- **조건**: `if (val || !result.endsWith(sep))` — 값이 있으면 항상 추가, 값이 없으면 이미 같은 구분자로 끝나면 스킵 + +### 9. 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경 + +- **결정**: `extractManualValuesFromInput` 내부의 카테고리/참조 빈 값 반환을 `"CATEGORY"`/`"REF"` → `""`로 변경 +- **근거**: 실제 코드 생성에서 빈 카테고리는 `""`인데 템플릿에서 `"CATEGORY"`를 쓰면 구조 불일치로 추출 실패. 로그로 확인: `userInputCode=-제발-015, previewTemplate=CATEGORY-____-XXX, extractedManualValues=[]` +- **카테고리 있을 때**: `catMapping2?.format` 반환은 수정 전후 동일하여 영향 없음 + +--- + +## 관련 파일 위치 + +| 구분 | 파일 경로 | 설명 | +|------|----------|------| +| 수정 대상 | `backend-node/src/services/numberingRuleService.ts` | joinPartsWithSeparators(L36), buildPrefixKey(L75), extractManualValuesFromInput(신규), allocateCode(L1296) | +| 신규 생성 | `db/migrations/1053_remove_bulk1_manual_config_value.sql` | BULK1 유령 값 정리 마이그레이션 | +| 변경 없음 | `frontend/components/screen/widgets/TextInputComponent.tsx` | _numberingRuleId 유지 확인 완료 | +| 변경 없음 | `frontend/lib/registry/components/numbering-rule/config.ts` | 채번 설정 레지스트리 | +| 변경 없음 | `frontend/components/screen/config-panels/NumberConfigPanel.tsx` | 채번 규칙 설정 패널 | +| 참고 | `backend-node/src/controllers/numberingRuleController.ts` | allocateNumberingCode 컨트롤러 | + +--- + +## 기술 참고 + +### allocateCode 실행 순서 (변경 전 → 후) + +``` +변경 전: buildPrefixKey(L1306) → 시퀀스 할당 → 수동 값 추출(L1332) → 코드 조합 +변경 후: 수동 값 추출 → buildPrefixKey(수동 값 포함) → 시퀀스 할당 → 코드 조합 +``` + +### prefix_key 구성 (변경 전 → 후) + +``` +변경 전: "카테고리값" (수동 파트 무시, 모든 접두어가 같은 키) +변경 후: "카테고리값|ㅁㅁㅁ" (수동 파트 포함, 접두어별 독립 키) +``` + +### 폴백 체인 (변경 전 → 후) + +``` +변경 전: extractedManualValues[i] || manualConfig.value || "" +변경 후: extractedManualValues[i] || "" +``` + +### joinPartsWithSeparators 연속 구분자 방지 (변경 전 → 후) + +``` +변경 전: "" + "-" + "" + "-" + "ㅁㅁㅁ" → "--ㅁㅁㅁ" +변경 후: "" + "-" (이미 "-"로 끝남, 스킵) + "ㅁㅁㅁ" → "-ㅁㅁㅁ" +``` + +### 템플릿 정합성 (변경 전 → 후) + +``` +변경 전: 카테고리 비었을 때 템플릿 = "CATEGORY-____-XXX" / 입력 = "-제발-015" → 불일치 → 추출 실패 +변경 후: 카테고리 비었을 때 템플릿 = "-____-XXX" / 입력 = "-제발-015" → 일치 → 추출 성공 +``` + +### BULK1이 DB에 남아있는 이유 + +``` +ManualConfigPanel.tsx: placeholder 입력란만 존재 (value 입력란 없음) +플레이스홀더 수정 시: { ...existingConfig, placeholder: newValue } +→ 기존 config에 value: "BULK1"이 있으면 스프레드로 계속 보존됨 +→ UI에서 제거 불가능한 유령 값 +``` diff --git a/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md new file mode 100644 index 00000000..803c679e --- /dev/null +++ b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md @@ -0,0 +1,79 @@ +# [체크리스트] 품번 수동 접두어 채번 - 접두어별 독립 순번 생성 + +> 관련 문서: [계획서](./MPN[계획]-품번-수동접두어채번.md) | [맥락노트](./MPN[맥락]-품번-수동접두어채번.md) + +--- + +## 공정 상태 + +- 전체 진행률: **85%** (코드 구현 + DB 마이그레이션 완료, 검증 대기) +- 현재 단계: 검증 대기 + +--- + +## 구현 체크리스트 + +### 1단계: 구조적 변경 (행위 변경 없음) + +- [x] `numberingRuleService.ts`에서 수동 값 추출 로직을 `extractManualValuesFromInput` private 메서드로 분리 +- [x] 기존 `allocateCode` 내부에서 분리한 메서드 호출로 교체 +- [x] 기존 동작과 동일한지 확인 (구조적 변경만, 행위 변경 없음) + +### 2단계: buildPrefixKey 수정 + +- [x] `buildPrefixKey` 시그니처에 `manualValues?: string[]` 파라미터 추가 +- [x] 수동 파트 처리 로직 변경: `continue` → `manualValues`에서 값 꺼내 `prefixParts`에 추가 +- [x] `previewCode` 호출부에 영향 없음 확인 (optional 파라미터) + +### 3단계: allocateCode 순서 변경 + 폴백 정리 + +- [x] 수동 값 추출 로직을 `buildPrefixKey` 호출 전으로 이동 +- [x] 수동 파트 1개 + 추출 실패 시 `userInputCode` 전체를 수동 값으로 사용하는 폴백 추가 +- [x] `buildPrefixKey` 호출 시 `extractedManualValues`를 세 번째 인자로 전달 +- [x] 코드 조합 단계에서 `part.manualConfig?.value` 폴백 제거 + +### 4단계: DB 마이그레이션 + +- [x] `db/migrations/1053_remove_bulk1_manual_config_value.sql` 작성 +- [x] `manual_config->>'value' = 'BULK1'` 조건으로 JSONB에서 `value` 키 제거 +- [x] 마이그레이션 실행 (9건 정리 완료) + +### 5단계: 연속 구분자(--) 방지 + +- [x] `joinPartsWithSeparators`에서 빈 파트 뒤 연속 구분자 방지 로직 추가 +- [x] `extractManualValuesFromInput`에서 카테고리/참조 빈 값 시 `""` 반환 (템플릿 정합성) + +### 6단계: 검증 + +- [ ] 카테고리 선택 + 수동입력 "ㅁㅁㅁ" → 카테고리값-ㅁㅁㅁ-001 생성 확인 +- [ ] 카테고리 미선택 + 수동입력 "ㅁㅁㅁ" → -ㅁㅁㅁ-001 생성 확인 (-- 아님) +- [ ] 같은 접두어 "ㅁㅁㅁ" 재등록 → -ㅁㅁㅁ-002 순번 증가 확인 +- [ ] 다른 접두어 "ㅇㅇㅇ" 등록 → -ㅇㅇㅇ-001 독립 시퀀스 확인 +- [ ] 수동 파트 없는 채번 규칙 동작 영향 없음 확인 +- [ ] previewCode (미리보기) 동작 영향 없음 확인 +- [ ] BULK1이 더 이상 생성되지 않음 확인 + +### 7단계: 정리 + +- [x] 린트 에러 없음 확인 +- [x] 계획서/맥락노트/체크리스트 최신화 + +--- + +## 알려진 이슈 (보류) + +| 이슈 | 설명 | 상태 | +|------|------|------| +| 저장 실패 시 순번 갭 | allocateCode와 saveFormData가 별도 트랜잭션이라 저장 실패해도 순번 소비됨 | 보류 | +| 유령 데이터 | 중복 품명으로 간헐적 저장 성공 + 리스트 미노출 | 보류 | + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-03-11 | 계획서, 맥락노트, 체크리스트 작성 완료 | +| 2026-03-11 | 1-4단계 구현 완료 | +| 2026-03-11 | 5단계 추가 구현 (연속 구분자 방지 + 템플릿 정합성 복원) | +| 2026-03-11 | 계맥체 최신화 완료. 문제 4-5 보류 | From 1a11b08487301d8d59f7b017da59e51d6027e47d Mon Sep 17 00:00:00 2001 From: syc0123 Date: Thu, 12 Mar 2026 16:07:13 +0900 Subject: [PATCH 02/18] feat: implement real-time numbering preview with manual input handling - Enhanced the `previewCode` endpoint to accept a new `manualInputValue` parameter, allowing for dynamic sequence generation based on user input. - Updated the `NumberingRuleService` to skip legacy sequence lookups when manual input is not provided, ensuring accurate initial sequence display. - Integrated debounce functionality in the `V2Input` component to optimize API calls for real-time suffix updates as users type. - Refactored category resolution logic into a helper function to reduce code duplication and improve maintainability. These changes significantly improve the user experience by providing immediate feedback on numbering sequences based on manual inputs. --- .../controllers/numberingRuleController.ts | 5 +- .../src/services/numberingRuleService.ts | 295 +++++------------- .../MPN[계획]-품번-수동접두어채번.md | 61 +++- .../MPN[맥락]-품번-수동접두어채번.md | 31 ++ .../MPN[체크]-품번-수동접두어채번.md | 24 +- frontend/components/v2/V2Input.tsx | 45 ++- frontend/lib/api/numberingRule.ts | 2 + 7 files changed, 239 insertions(+), 224 deletions(-) diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index a3887ab8..2e3d033b 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -311,13 +311,14 @@ router.post( async (req: AuthenticatedRequest, res: Response) => { const companyCode = req.user!.companyCode; const { ruleId } = req.params; - const { formData } = req.body; // 폼 데이터 (카테고리 기반 채번 시 사용) + const { formData, manualInputValue } = req.body; try { const previewCode = await numberingRuleService.previewCode( ruleId, companyCode, - formData + formData, + manualInputValue ); return res.json({ success: true, data: { generatedCode: previewCode } }); } catch (error: any) { diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index f4175b9d..80a96cb3 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -1086,22 +1086,30 @@ class NumberingRuleService { * @param ruleId 채번 규칙 ID * @param companyCode 회사 코드 * @param formData 폼 데이터 (카테고리 기반 채번 시 사용) + * @param manualInputValue 수동 입력 값 (접두어별 순번 조회용) */ async previewCode( ruleId: string, companyCode: string, - formData?: Record + formData?: Record, + manualInputValue?: string ): Promise { const rule = await this.getRuleById(ruleId, companyCode); if (!rule) throw new Error("규칙을 찾을 수 없습니다"); - // prefix_key 기반 순번 조회 - const prefixKey = await this.buildPrefixKey(rule, formData); + // 수동 파트가 있는데 입력값이 없으면 레거시 공용 시퀀스 조회를 건너뜀 + const hasManualPart = rule.parts.some((p: any) => p.generationMethod === "manual"); + const skipSequenceLookup = hasManualPart && !manualInputValue; + + const manualValues = manualInputValue ? [manualInputValue] : undefined; + const prefixKey = await this.buildPrefixKey(rule, formData, manualValues); const pool = getPool(); - const currentSeq = await this.getSequenceForPrefix(pool, ruleId, companyCode, prefixKey); + const currentSeq = skipSequenceLookup + ? 0 + : await this.getSequenceForPrefix(pool, ruleId, companyCode, prefixKey); logger.info("미리보기: prefix_key 기반 순번 조회", { - ruleId, prefixKey, currentSeq, + ruleId, prefixKey, currentSeq, skipSequenceLookup, }); const parts = await Promise.all(rule.parts @@ -1116,7 +1124,8 @@ class NumberingRuleService { switch (part.partType) { case "sequence": { const length = autoConfig.sequenceLength || 3; - const nextSequence = currentSeq + 1; + const startFrom = autoConfig.startFrom || 1; + const nextSequence = currentSeq + startFrom; return String(nextSequence).padStart(length, "0"); } @@ -1158,110 +1167,8 @@ class NumberingRuleService { return autoConfig.textValue || "TEXT"; } - case "category": { - // 카테고리 기반 코드 생성 - const categoryKey = autoConfig.categoryKey; // 예: "item_info.material" - const categoryMappings = autoConfig.categoryMappings || []; - - if (!categoryKey || !formData) { - logger.warn("카테고리 키 또는 폼 데이터 없음", { - categoryKey, - hasFormData: !!formData, - }); - return ""; - } - - // categoryKey에서 컬럼명 추출 (예: "item_info.material" -> "material") - const columnName = categoryKey.includes(".") - ? categoryKey.split(".")[1] - : categoryKey; - - // 폼 데이터에서 해당 컬럼의 값 가져오기 - const selectedValue = formData[columnName]; - - logger.info("카테고리 파트 처리", { - categoryKey, - columnName, - selectedValue, - formDataKeys: Object.keys(formData), - mappingsCount: categoryMappings.length, - }); - - if (!selectedValue) { - logger.warn("카테고리 값이 선택되지 않음", { - columnName, - formDataKeys: Object.keys(formData), - }); - return ""; - } - - // 카테고리 매핑에서 해당 값에 대한 형식 찾기 - // selectedValue는 valueCode일 수 있음 (V2Select에서 valueCode를 value로 사용) - const selectedValueStr = String(selectedValue); - let mapping = categoryMappings.find((m: any) => { - // ID로 매칭 (기존 방식: V2Select가 valueId를 사용하던 경우) - if (m.categoryValueId?.toString() === selectedValueStr) - return true; - // valueCode로 매칭 (매핑에 categoryValueCode가 있는 경우) - if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) - return true; - // 라벨로 매칭 (폴백) - if (m.categoryValueLabel === selectedValueStr) return true; - return false; - }); - - // 매핑을 못 찾았으면 category_values 테이블에서 valueCode → valueId 역변환 시도 - if (!mapping) { - try { - const pool = getPool(); - const [catTableName, catColumnName] = categoryKey.includes(".") - ? categoryKey.split(".") - : [categoryKey, categoryKey]; - const cvResult = await pool.query( - `SELECT value_id, value_code, value_label FROM category_values - WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, - [catTableName, catColumnName, selectedValueStr] - ); - if (cvResult.rows.length > 0) { - const resolvedId = cvResult.rows[0].value_id; - const resolvedLabel = cvResult.rows[0].value_label; - mapping = categoryMappings.find((m: any) => { - if (m.categoryValueId?.toString() === String(resolvedId)) return true; - if (m.categoryValueLabel === resolvedLabel) return true; - return false; - }); - if (mapping) { - logger.info("카테고리 매핑 역변환 성공 (valueCode→valueId)", { - valueCode: selectedValueStr, - resolvedId, - resolvedLabel, - format: mapping.format, - }); - } - } - } catch (lookupError: any) { - logger.warn("카테고리 값 역변환 조회 실패", { error: lookupError.message }); - } - } - - if (mapping) { - logger.info("카테고리 매핑 적용", { - selectedValue, - format: mapping.format, - categoryValueLabel: mapping.categoryValueLabel, - }); - return mapping.format || ""; - } - - logger.warn("카테고리 매핑을 찾을 수 없음", { - selectedValue, - availableMappings: categoryMappings.map((m: any) => ({ - id: m.categoryValueId, - label: m.categoryValueLabel, - })), - }); - return ""; - } + case "category": + return this.resolveCategoryFormat(autoConfig, formData); case "reference": { const refColumn = autoConfig.referenceColumnName; @@ -1364,7 +1271,9 @@ class NumberingRuleService { switch (part.partType) { case "sequence": { const length = autoConfig.sequenceLength || 3; - return String(allocatedSequence).padStart(length, "0"); + const startFrom = autoConfig.startFrom || 1; + const actualSequence = allocatedSequence + startFrom - 1; + return String(actualSequence).padStart(length, "0"); } case "number": { @@ -1401,65 +1310,14 @@ class NumberingRuleService { return autoConfig.textValue || "TEXT"; } - case "category": { - const categoryKey = autoConfig.categoryKey; - const categoryMappings = autoConfig.categoryMappings || []; - - if (!categoryKey || !formData) { - return ""; - } - - const columnName = categoryKey.includes(".") - ? categoryKey.split(".")[1] - : categoryKey; - - const selectedValue = formData[columnName]; - - if (!selectedValue) { - return ""; - } - - const selectedValueStr = String(selectedValue); - let allocMapping = categoryMappings.find((m: any) => { - if (m.categoryValueId?.toString() === selectedValueStr) return true; - if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) return true; - if (m.categoryValueLabel === selectedValueStr) return true; - return false; - }); - - if (!allocMapping) { - try { - const pool3 = getPool(); - const [ct3, cc3] = categoryKey.includes(".") ? categoryKey.split(".") : [categoryKey, categoryKey]; - const cvr3 = await pool3.query( - `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, - [ct3, cc3, selectedValueStr] - ); - if (cvr3.rows.length > 0) { - const rid3 = cvr3.rows[0].value_id; - const rlabel3 = cvr3.rows[0].value_label; - allocMapping = categoryMappings.find((m: any) => { - if (m.categoryValueId?.toString() === String(rid3)) return true; - if (m.categoryValueLabel === rlabel3) return true; - return false; - }); - } - } catch { /* ignore */ } - } - - if (allocMapping) { - return allocMapping.format || ""; - } - - return ""; - } + case "category": + return this.resolveCategoryFormat(autoConfig, formData); case "reference": { const refColumn = autoConfig.referenceColumnName; if (refColumn && formData && formData[refColumn]) { return String(formData[refColumn]); } - logger.warn("reference 파트: 참조 컬럼 값 없음", { refColumn, formDataKeys: formData ? Object.keys(formData) : [] }); return ""; } @@ -1525,57 +1383,12 @@ class NumberingRuleService { return autoConfig.textValue || ""; case "date": return "DATEPART"; - case "category": { - const catKey2 = autoConfig.categoryKey; - const catMappings2 = autoConfig.categoryMappings || []; - - if (!catKey2 || !formData) { - return ""; - } - - const colName2 = catKey2.includes(".") - ? catKey2.split(".")[1] - : catKey2; - const selVal2 = formData[colName2]; - - if (!selVal2) { - return ""; - } - - const selValStr2 = String(selVal2); - let catMapping2 = catMappings2.find((m: any) => { - if (m.categoryValueId?.toString() === selValStr2) return true; - if (m.categoryValueCode && m.categoryValueCode === selValStr2) return true; - if (m.categoryValueLabel === selValStr2) return true; - return false; - }); - - if (!catMapping2) { - try { - const pool2 = getPool(); - const [ct2, cc2] = catKey2.includes(".") ? catKey2.split(".") : [catKey2, catKey2]; - const cvr2 = await pool2.query( - `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, - [ct2, cc2, selValStr2] - ); - if (cvr2.rows.length > 0) { - const rid2 = cvr2.rows[0].value_id; - const rlabel2 = cvr2.rows[0].value_label; - catMapping2 = catMappings2.find((m: any) => { - if (m.categoryValueId?.toString() === String(rid2)) return true; - if (m.categoryValueLabel === rlabel2) return true; - return false; - }); - } - } catch { /* ignore */ } - } - - return catMapping2?.format || ""; - } + case "category": + return this.resolveCategoryFormat(autoConfig, formData); case "reference": { - const refCol2 = autoConfig.referenceColumnName; - if (refCol2 && formData && formData[refCol2]) { - return String(formData[refCol2]); + const refColumn = autoConfig.referenceColumnName; + if (refColumn && formData && formData[refColumn]) { + return String(formData[refColumn]); } return ""; } @@ -1622,6 +1435,60 @@ class NumberingRuleService { return extractedValues; } + /** + * 카테고리 매핑에서 format 값을 해석 + * categoryKey + formData로 선택된 값을 찾고, 매핑 테이블에서 format 반환 + */ + private async resolveCategoryFormat( + autoConfig: Record, + formData?: Record + ): Promise { + const categoryKey = autoConfig.categoryKey; + const categoryMappings = autoConfig.categoryMappings || []; + + if (!categoryKey || !formData) return ""; + + const columnName = categoryKey.includes(".") + ? categoryKey.split(".")[1] + : categoryKey; + const selectedValue = formData[columnName]; + + if (!selectedValue) return ""; + + const selectedValueStr = String(selectedValue); + let mapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === selectedValueStr) return true; + if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) return true; + if (m.categoryValueLabel === selectedValueStr) return true; + return false; + }); + + // 매핑 못 찾으면 category_values에서 valueCode → valueId 역변환 + if (!mapping) { + try { + const pool = getPool(); + const [tableName, colName] = categoryKey.includes(".") + ? categoryKey.split(".") + : [categoryKey, categoryKey]; + const result = await pool.query( + `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`, + [tableName, colName, selectedValueStr] + ); + if (result.rows.length > 0) { + const resolvedId = result.rows[0].value_id; + const resolvedLabel = result.rows[0].value_label; + mapping = categoryMappings.find((m: any) => { + if (m.categoryValueId?.toString() === String(resolvedId)) return true; + if (m.categoryValueLabel === resolvedLabel) return true; + return false; + }); + } + } catch { /* ignore */ } + } + + return mapping?.format || ""; + } + private formatDate(date: Date, format: string): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); diff --git a/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md index b3337f9e..0cac81c2 100644 --- a/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md +++ b/docs/ycshin-node/MPN[계획]-품번-수동접두어채번.md @@ -194,17 +194,17 @@ sequenceDiagram | 파일 | 변경 내용 | 규모 | |------|----------|------| -| `backend-node/src/services/numberingRuleService.ts` | `buildPrefixKey`에 `manualValues` 파라미터 추가, `allocateCode`에서 수동 값 추출 순서 변경 + 폴백 체인 정리, `extractManualValuesFromInput` 헬퍼 분리, `joinPartsWithSeparators` 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경 | ~60줄 | +| `backend-node/src/services/numberingRuleService.ts` | `buildPrefixKey`에 `manualValues` 파라미터 추가, `allocateCode`에서 수동 값 추출 순서 변경 + 폴백 체인 정리, `extractManualValuesFromInput` 헬퍼 분리, `joinPartsWithSeparators` 연속 구분자 방지, 템플릿 카테고리/참조 플레이스홀더를 실제값으로 변경, `previewCode`에 `manualInputValue` 파라미터 추가 + `startFrom` 적용 | ~80줄 | +| `backend-node/src/controllers/numberingRuleController.ts` | preview 엔드포인트에 `manualInputValue` body 파라미터 수신 추가 | ~2줄 | +| `frontend/lib/api/numberingRule.ts` | `previewNumberingCode`에 `manualInputValue` 파라미터 추가 | ~3줄 | +| `frontend/components/v2/V2Input.tsx` | 수동 입력값 변경 시 디바운스(300ms) preview API 호출 + suffix(순번) 실시간 갱신 | ~35줄 | | `db/migrations/1053_remove_bulk1_manual_config_value.sql` | `numbering_rule_parts.manual_config`에서 `value: "BULK1"` 제거 | SQL 1건 | -> `TextInputComponent.tsx` 변경 불필요. `_numberingRuleId`가 유지되고 있으며, 수동 값 추출도 정상 동작 확인됨. -> 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요. - ### buildPrefixKey 호출부 영향 분석 | 호출부 | 위치 | `manualValues` 전달 | 영향 | |--------|------|---------------------|------| -| `previewCode` | L1091 | 미전달 (undefined) | 변화 없음 | +| `previewCode` | L1091 | `manualInputValue` 전달 시 포함 | 접두어별 정확한 순번 조회 | | `allocateCode` | L1332 | 전달 | prefix_key에 수동 값 포함됨 | ### 멀티테넌시 체크 @@ -367,3 +367,54 @@ WHERE generation_method = 'manual' - 프론트엔드 변경 없음 → 프론트엔드 테스트 불필요 - `joinPartsWithSeparators`는 연속 구분자만 방지, 기존 구분자 구조 유지 - 템플릿 카테고리/참조 빈 값을 실제 코드와 일치시켜 추출 성공률 향상 + +--- + +## 실시간 순번 미리보기 (추가 기능) + +### 배경 + +품목 등록 모달에서 수동 입력 세그먼트 우측에 표시되는 순번(suffix)이 입력값과 무관하게 고정되어 있었음. 사용자가 "ㅇㅇ"을 입력하면 해당 접두어로 이미 몇 개가 등록되었는지에 따라 순번이 달라져야 함. + +### 목표 동작 + +``` +모달 열림 : -[입력하시오]-005 (startFrom=5 기반 기본 순번) +"ㅇㅇ" 입력 : -[ㅇㅇ]-005 (기존 "ㅇㅇ" 등록 0건) +저장 후 재입력 "ㅇㅇ": -[ㅇㅇ]-006 (기존 "ㅇㅇ" 등록 1건) +``` + +### 아키텍처 + +```mermaid +sequenceDiagram + participant User as 사용자 + participant V2 as V2Input + participant API as previewNumberingCode + participant BE as numberingRuleService.previewCode + participant DB as numbering_rule_sequences + + User->>V2: 수동 입력 "ㅇㅇ" + Note over V2: 디바운스 300ms + V2->>API: preview(ruleId, formData, "ㅇㅇ") + API->>BE: previewCode(ruleId, companyCode, formData, "ㅇㅇ") + BE->>BE: buildPrefixKey(rule, formData, ["ㅇㅇ"]) + Note over BE: prefix_key = "카테고리|ㅇㅇ" + BE->>DB: getSequenceForPrefix(prefix_key) + DB-->>BE: currentSeq = 0 + Note over BE: nextSequence = 0 + startFrom(5) = 5 + BE-->>API: "-____-005" + API-->>V2: generatedCode + V2->>V2: suffix = "-005" 갱신 + Note over V2: 화면 표시: -[ㅇㅇ]-005 +``` + +### 변경 내용 + +1. **백엔드 컨트롤러**: preview 엔드포인트가 `req.body.manualInputValue` 수신 +2. **백엔드 서비스**: `previewCode`가 `manualInputValue`를 받아 `buildPrefixKey`에 전달 → 접두어별 정확한 시퀀스 조회 +3. **백엔드 서비스**: 수동 파트가 있는데 `manualInputValue`가 없는 초기 상태 → 레거시 공용 시퀀스 조회 건너뜀, `currentSeq = 0` 사용 → `startFrom` 기본값 표시 +4. **프론트엔드 API**: `previewNumberingCode`에 `manualInputValue` 파라미터 추가 +5. **V2Input**: `manualInputValue` 변경 시 디바운스(300ms) preview API 재호출 → `numberingTemplateRef` 갱신 → suffix 실시간 업데이트 +6. **V2Input**: 카테고리 변경 시 초기 useEffect에서도 현재 `manualInputValue`를 preview에 전달 → 카테고리 변경/삭제 시 순번 즉시 반영 +7. **코드 정리**: 카테고리 해석 로직 3곳 중복 → `resolveCategoryFormat` 헬퍼로 통합 (약 100줄 감소) diff --git a/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md index 9ff76513..1d895989 100644 --- a/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md +++ b/docs/ycshin-node/MPN[맥락]-품번-수동접두어채번.md @@ -120,6 +120,37 @@ 변경 후: 카테고리 비었을 때 템플릿 = "-____-XXX" / 입력 = "-제발-015" → 일치 → 추출 성공 ``` +### 10. 실시간 순번 미리보기 구현 방식 + +- **결정**: V2Input에서 `manualInputValue` 변경 시 디바운스(300ms)로 preview API를 재호출하여 suffix(순번)를 갱신 +- **근거**: 기존 preview API는 `manualInputValue` 없이 호출되어 모든 접두어가 같은 기본 순번을 표시함. 접두어별 정확한 순번을 보여주려면 preview 시점에도 수동 값을 전달하여 해당 prefix_key의 시퀀스를 조회해야 함 +- **대안 검토**: 프론트엔드에서 카운트 API를 별도 호출 → 기각 (기존 `previewCode` 흐름 재사용이 프로젝트 관행에 부합) +- **디바운스 300ms**: 사용자 타이핑 중 과도한 API 호출 방지. 프로젝트 기존 패턴(검색 디바운스 등)과 동일 + +### 11. previewCode에 manualInputValue 전달 + +- **결정**: `previewCode` 시그니처에 `manualInputValue?: string` 추가, `buildPrefixKey`에 `[manualInputValue]`로 전달 +- **근거**: `buildPrefixKey`가 이미 `manualValues` optional 파라미터를 지원하므로 자연스럽게 확장 가능. 순번 조회 시 접두어별 독립 시퀀스를 정확히 반영함 +- **하위호환**: optional 파라미터이므로 기존 호출(`formData`만 전달)에 영향 없음 + +### 12. 초기 상태에서 레거시 시퀀스 조회 방지 + +- **결정**: `previewCode`에서 수동 파트가 있는데 `manualInputValue`가 없으면 시퀀스 조회를 건너뛰고 `currentSeq = 0` 사용 +- **근거**: 수정 전에는 모든 할당이 수동 파트 없는 공용 prefix_key를 사용했으므로 레거시 시퀀스가 누적되어 있음(예: 16). 모달 초기 상태에서 이 공용 키를 조회하면 `-016`이 표시됨. 아직 어떤 접두어인지 모르는 상태이므로 `startFrom` 기본값을 보여주는 것이 정확함 +- **`currentSeq = 0` + `startFrom`**: `nextSequence = 0 + startFrom(5) = 5` → `-005` 표시. 사용자가 입력하면 디바운스 preview가 해당 접두어의 실제 시퀀스를 조회 + +### 13. 카테고리 변경 시 수동 입력값 포함하여 순번 재조회 + +- **결정**: 초기 useEffect(카테고리 변경 트리거)에서 `previewNumberingCode` 호출 시 현재 `manualInputValue`도 함께 전달 +- **근거**: 카테고리를 바꾸거나 삭제하면 prefix_key가 달라지므로 순번도 달라져야 함. 기존에는 입력값 변경과 카테고리 변경이 별도 트리거여서 카테고리 변경 시 수동 값이 누락됨 +- **빈 입력값 처리**: `manualInputValue || undefined`로 처리하여 빈 문자열일 때는 기존처럼 `skipSequenceLookup` 작동 + +### 14. 카테고리 해석 로직 resolveCategoryFormat 헬퍼 통합 + +- **결정**: `previewCode`, `allocateCode`, `extractManualValuesFromInput` 3곳에 복붙된 카테고리 매핑 해석 로직을 `resolveCategoryFormat` private 메서드로 추출 +- **근거**: 동일 로직 약 50줄이 3곳에 복사되어 있었음 (변수명만 pool2/ct2/cc2 등으로 다름). 한 곳을 수정하면 나머지도 동일하게 수정해야 하는 유지보수 위험 +- **원칙**: 구조적 변경만 수행 (로직 변경 없음) + ### BULK1이 DB에 남아있는 이유 ``` diff --git a/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md index 803c679e..b74eed58 100644 --- a/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md +++ b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md @@ -6,7 +6,7 @@ ## 공정 상태 -- 전체 진행률: **85%** (코드 구현 + DB 마이그레이션 완료, 검증 대기) +- 전체 진행률: **95%** (코드 구현 + DB 마이그레이션 + 실시간 미리보기 + 코드 정리 완료, 검증 대기) - 현재 단계: 검증 대기 --- @@ -53,9 +53,24 @@ - [ ] previewCode (미리보기) 동작 영향 없음 확인 - [ ] BULK1이 더 이상 생성되지 않음 확인 -### 7단계: 정리 +### 7단계: 실시간 순번 미리보기 +- [x] 백엔드 컨트롤러: preview 엔드포인트에 `manualInputValue` body 파라미터 수신 추가 +- [x] 백엔드 서비스: `previewCode`에 `manualInputValue` 파라미터 추가, `buildPrefixKey`에 전달 +- [x] 프론트엔드 API: `previewNumberingCode`에 `manualInputValue` 파라미터 추가 +- [x] V2Input: `manualInputValue` 변경 시 디바운스(300ms) preview API 호출 + suffix 갱신 +- [x] 백엔드 서비스: 초기 상태(수동 입력 없음) 시 레거시 공용 시퀀스 조회 건너뜀 → startFrom 기본값 표시 +- [x] V2Input: 카테고리 변경 시 초기 useEffect에서도 `manualInputValue` 전달 → 순번 즉시 반영 - [x] 린트 에러 없음 확인 + +### 8단계: 코드 정리 + +- [x] 카테고리 해석 로직 3곳 중복 → `resolveCategoryFormat` 헬퍼 추출 (약 100줄 감소) +- [x] 임시 변수명 정리 (pool2/ct2/cc2 등 복붙 흔적 제거) +- [x] 린트 에러 없음 확인 + +### 9단계: 정리 + - [x] 계획서/맥락노트/체크리스트 최신화 --- @@ -77,3 +92,8 @@ | 2026-03-11 | 1-4단계 구현 완료 | | 2026-03-11 | 5단계 추가 구현 (연속 구분자 방지 + 템플릿 정합성 복원) | | 2026-03-11 | 계맥체 최신화 완료. 문제 4-5 보류 | +| 2026-03-12 | 7단계 실시간 순번 미리보기 구현 완료 (백엔드/프론트엔드 4파일) | +| 2026-03-12 | 계맥체 최신화 완료 | +| 2026-03-12 | 초기 상태 레거시 시퀀스 조회 방지 수정 + 계맥체 반영 | +| 2026-03-12 | 카테고리 변경 시 수동 입력값 포함 순번 재조회 수정 | +| 2026-03-12 | resolveCategoryFormat 헬퍼 추출 코드 정리 + 계맥체 최신화 | diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index 2d7c3246..94bd3ea8 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -676,7 +676,7 @@ export const V2Input = forwardRef((props, ref) => // 채번 코드 생성 (formDataRef.current 사용하여 최신 formData 전달) const currentFormData = formDataRef.current; - const previewResponse = await previewNumberingCode(numberingRuleId, currentFormData); + const previewResponse = await previewNumberingCode(numberingRuleId, currentFormData, manualInputValue || undefined); if (previewResponse.success && previewResponse.data?.generatedCode) { const generatedCode = previewResponse.data.generatedCode; @@ -764,6 +764,49 @@ export const V2Input = forwardRef((props, ref) => }; }, [columnName, manualInputValue, propsInputType, config.inputType, config.type]); + // 수동 입력값 변경 시 디바운스로 순번 미리보기 갱신 + useEffect(() => { + const inputType = propsInputType || config.inputType || config.type || "text"; + if (inputType !== "numbering") return; + if (!numberingTemplateRef.current?.includes("____")) return; + + const ruleId = numberingRuleIdRef.current; + if (!ruleId) return; + + // 사용자가 한 번도 입력하지 않은 초기 상태면 스킵 + if (!userEditedNumberingRef.current) return; + + const debounceTimer = setTimeout(async () => { + try { + const currentFormData = formDataRef.current; + const resp = await previewNumberingCode(ruleId, currentFormData, manualInputValue || undefined); + + if (resp.success && resp.data?.generatedCode) { + const newTemplate = resp.data.generatedCode; + if (newTemplate.includes("____")) { + numberingTemplateRef.current = newTemplate; + + const parts = newTemplate.split("____"); + const prefix = parts[0] || ""; + const suffix = parts.length > 1 ? parts.slice(1).join("") : ""; + const combined = prefix + manualInputValue + suffix; + + setAutoGeneratedValue(combined); + onChange?.(combined); + if (onFormDataChange && columnName) { + onFormDataChange(columnName, combined); + } + } + } + } catch { + /* 미리보기 실패 시 기존 suffix 유지 */ + } + }, 300); + + return () => clearTimeout(debounceTimer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [manualInputValue]); + // 실제 표시할 값 (자동생성 값 또는 props value) const displayValue = autoGeneratedValue ?? value; diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index b0ec38e2..01f0a321 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -105,6 +105,7 @@ export async function deleteNumberingRule(ruleId: string): Promise, + manualInputValue?: string, ): Promise> { // ruleId 유효성 검사 if (!ruleId || ruleId === "undefined" || ruleId === "null") { @@ -114,6 +115,7 @@ export async function previewNumberingCode( try { const response = await apiClient.post(`/numbering-rules/${ruleId}/preview`, { formData: formData || {}, + manualInputValue, }); if (!response.data) { return { success: false, error: "서버 응답이 비어있습니다" }; From a2c532c7c7676cfacc8a1c7de1fcbbcb9ef68100 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 12 Mar 2026 18:26:47 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EC=9E=A0=EA=B8=88=20+=20=EC=86=8C=EC=9C=A0=EC=9E=90=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=95=A1=EC=85=98=20=EC=A0=9C=EC=96=B4=20?= =?UTF-8?q?+=20=EB=94=94=EC=9E=90=EC=9D=B4=EB=84=88=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20UI=20=EB=8F=99=EC=8B=9C=20=EC=A0=91=EC=88=98=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=EB=B0=A9=EC=A7=80(preCondition=20WHERE=20+=20409?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC),=20=EC=86=8C=EC=9C=A0=EC=9E=90=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EC=8B=9C=EC=97=90=EB=A7=8C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=99=9C=EC=84=B1=ED=99=94(owner-match=20showCondi?= =?UTF-8?q?tion),=20=EB=B3=B8=EC=9D=B8=20=EC=B9=B4=EB=93=9C=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=20=EC=A0=95=EB=A0=AC(ownerSortColumn)=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EA=B3=A0=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B4=EB=84=88=EC=97=90=EC=84=9C=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20UI=203=EC=A2=85?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4.=20[=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C]=20-=20popActionRoutes:=20TaskBody=EC=97=90?= =?UTF-8?q?=20preCondition=20=EC=B6=94=EA=B0=80,=20data-update=20WHERE=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=82=BD=EC=9E=85,=20=20=20rowCount=3D0?= =?UTF-8?q?=20=EC=8B=9C=20409=20Conflict=20=EB=B0=98=ED=99=98=20(isPreCond?= =?UTF-8?q?itionFail)=20[=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20-?= =?UTF-8?q?=20=EB=9F=B0=ED=83=80=EC=9E=84]=20-=20types.ts:=20ActionPreCond?= =?UTF-8?q?ition=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4,=20owner-?= =?UTF-8?q?match=20=ED=83=80=EC=9E=85,=20ownerSortColumn=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20-=20cell-renderers:=20evaluateShowCondition?= =?UTF-8?q?=EC=97=90=20owner-match=20=EB=B6=84=EA=B8=B0=20+=20currentUserI?= =?UTF-8?q?d=20prop=20-=20PopCardListV2Component:=20useAuth=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99,=20preCondition=20=EC=A0=84=EB=8B=AC/409=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC,=20=20=20ownerSortColumn=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=A0=95=EB=A0=AC,=20currentUserId=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EC=A0=84=EB=8B=AC=20[=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20-=20=EB=94=94=EC=9E=90=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EC=84=A4=EC=A0=95=20UI]=20-=20PopCardListV2Config:?= =?UTF-8?q?=20showCondition=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EC=97=90=20"=EC=86=8C=EC=9C=A0=EC=9E=90=20=EC=9D=BC=EC=B9=98"?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20+=20=EC=BB=AC=EB=9F=BC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D,=20=20=20ImmediateActionEditor=EC=97=90=20"=EC=82=AC?= =?UTF-8?q?=EC=A0=84=20=EC=A1=B0=EA=B1=B4(=EC=A4=91=EB=B3=B5=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80)"=20=ED=86=A0=EA=B8=80=20+=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC/=EA=B8=B0=EB=8C=80=EA=B0=92/=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EB=A9=94=EC=8B=9C=EC=A7=80,=20=20=20TabActions?= =?UTF-8?q?=EC=97=90=20"=EC=86=8C=EC=9C=A0=EC=9E=90=20=EC=9A=B0=EC=84=A0?= =?UTF-8?q?=20=EC=A0=95=EB=A0=AC"=20=EC=BB=AC=EB=9F=BC=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/routes/popActionRoutes.ts | 59 +++- .../PopCardListV2Component.tsx | 168 ++++++++++- .../pop-card-list-v2/PopCardListV2Config.tsx | 272 ++++++++++++++++-- .../pop-card-list-v2/cell-renderers.tsx | 10 +- frontend/lib/registry/pop-components/types.ts | 31 +- 5 files changed, 494 insertions(+), 46 deletions(-) diff --git a/backend-node/src/routes/popActionRoutes.ts b/backend-node/src/routes/popActionRoutes.ts index d25c6bdc..669cc960 100644 --- a/backend-node/src/routes/popActionRoutes.ts +++ b/backend-node/src/routes/popActionRoutes.ts @@ -104,6 +104,11 @@ interface TaskBody { manualItemField?: string; manualPkColumn?: string; cartScreenId?: string; + preCondition?: { + column: string; + expectedValue: string; + failMessage?: string; + }; } function resolveStatusValue( @@ -334,14 +339,30 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp const item = items[i] ?? {}; const resolved = resolveStatusValue("conditional", task.fixedValue ?? "", task.conditionalValue, item); const autoUpdated = task.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : ""; - await client.query( - `UPDATE "${task.targetTable}" SET "${task.targetColumn}" = $1${autoUpdated} WHERE company_code = $2 AND "${pkColumn}" = $3`, - [resolved, companyCode, lookupValues[i]], + let condWhere = `WHERE company_code = $2 AND "${pkColumn}" = $3`; + const condParams: unknown[] = [resolved, companyCode, lookupValues[i]]; + if (task.preCondition?.column && task.preCondition?.expectedValue) { + if (!isSafeIdentifier(task.preCondition.column)) throw new Error(`유효하지 않은 preCondition 컬럼명: ${task.preCondition.column}`); + condWhere += ` AND "${task.preCondition.column}" = $4`; + condParams.push(task.preCondition.expectedValue); + } + const condResult = await client.query( + `UPDATE "${task.targetTable}" SET "${task.targetColumn}" = $1${autoUpdated} ${condWhere}`, + condParams, ); + if (task.preCondition && condResult.rowCount === 0) { + const err = new Error(task.preCondition.failMessage || "조건이 일치하지 않아 처리할 수 없습니다."); + (err as any).isPreConditionFail = true; + throw err; + } processedCount++; } } else if (opType === "db-conditional") { - // DB 컬럼 간 비교 후 값 판정 (CASE WHEN col_a >= col_b THEN '완료' ELSE '진행중') + if (task.preCondition) { + logger.warn("[pop/execute-action] db-conditional에는 preCondition 미지원, 무시됨", { + taskId: task.id, preCondition: task.preCondition, + }); + } if (!task.compareColumn || !task.compareOperator || !task.compareWith) break; if (!isSafeIdentifier(task.compareColumn) || !isSafeIdentifier(task.compareWith)) break; @@ -392,10 +413,24 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp } const autoUpdatedDate = task.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : ""; - await client.query( - `UPDATE "${task.targetTable}" SET ${setSql}${autoUpdatedDate} WHERE company_code = $2 AND "${pkColumn}" = $3`, - [value, companyCode, lookupValues[i]], + let whereSql = `WHERE company_code = $2 AND "${pkColumn}" = $3`; + const queryParams: unknown[] = [value, companyCode, lookupValues[i]]; + if (task.preCondition?.column && task.preCondition?.expectedValue) { + if (!isSafeIdentifier(task.preCondition.column)) { + throw new Error(`유효하지 않은 preCondition 컬럼명: ${task.preCondition.column}`); + } + whereSql += ` AND "${task.preCondition.column}" = $4`; + queryParams.push(task.preCondition.expectedValue); + } + const updateResult = await client.query( + `UPDATE "${task.targetTable}" SET ${setSql}${autoUpdatedDate} ${whereSql}`, + queryParams, ); + if (task.preCondition && updateResult.rowCount === 0) { + const err = new Error(task.preCondition.failMessage || "조건이 일치하지 않아 처리할 수 없습니다."); + (err as any).isPreConditionFail = true; + throw err; + } processedCount++; } } @@ -746,6 +781,16 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp }); } catch (error: any) { await client.query("ROLLBACK"); + + if (error.isPreConditionFail) { + logger.warn("[pop/execute-action] preCondition 실패", { message: error.message }); + return res.status(409).json({ + success: false, + message: error.message, + errorCode: "PRE_CONDITION_FAIL", + }); + } + logger.error("[pop/execute-action] 오류:", error); return res.status(500).json({ success: false, diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 55829efb..3a52a36e 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -34,6 +34,7 @@ import type { TimelineDataSource, ActionButtonUpdate, ActionButtonClickAction, + QuantityInputConfig, StatusValueMapping, SelectModeConfig, SelectModeButtonConfig, @@ -47,6 +48,7 @@ import { screenApi } from "@/lib/api/screen"; import { apiClient } from "@/lib/api/client"; import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useCartSync } from "@/hooks/pop/useCartSync"; +import { useAuth } from "@/hooks/useAuth"; import { NumberInputModal } from "../pop-card-list/NumberInputModal"; import { renderCellV2 } from "./cell-renderers"; import type { PopLayoutDataV5 } from "@/components/pop/designer/types/pop-layout"; @@ -56,6 +58,32 @@ const PopViewerWithModals = dynamic(() => import("@/components/pop/viewer/PopVie type RowData = Record; +function calculateMaxQty( + row: RowData, + processId: string | number | undefined, + cfg?: QuantityInputConfig, +): number { + if (!cfg) return 999999; + const maxVal = cfg.maxColumn ? Number(row[cfg.maxColumn]) || 999999 : 999999; + if (!cfg.currentColumn) return maxVal; + + const processFlow = row.__processFlow__ as Array<{ + isCurrent: boolean; + processId?: string | number; + rawData?: Record; + }> | undefined; + + const currentProcess = processId + ? processFlow?.find((p) => String(p.processId) === String(processId)) + : processFlow?.find((p) => p.isCurrent); + + if (currentProcess?.rawData) { + const currentVal = Number(currentProcess.rawData[cfg.currentColumn]) || 0; + return Math.max(0, maxVal - currentVal); + } + return maxVal; +} + // cart_items 행 파싱 (pop-card-list에서 그대로 차용) function parseCartRow(dbRow: Record): Record { let rowData: Record = {}; @@ -113,6 +141,7 @@ export function PopCardListV2Component({ }: PopCardListV2ComponentProps) { const { subscribe, publish } = usePopEvent(screenId || "default"); const router = useRouter(); + const { userId: currentUserId } = useAuth(); const isCartListMode = config?.cartListMode?.enabled === true; const [inheritedConfig, setInheritedConfig] = useState | null>(null); @@ -469,7 +498,7 @@ export function PopCardListV2Component({ type: "data-update" as const, targetTable: btnConfig.targetTable!, targetColumn: u.column, - operationType: "assign" as const, + operationType: (u.operationType || "assign") as "assign" | "add" | "subtract", valueSource: "fixed" as const, fixedValue: u.valueType === "static" ? (u.value ?? "") : u.valueType === "currentUser" ? "__CURRENT_USER__" : @@ -619,11 +648,28 @@ export function PopCardListV2Component({ const scrollAreaRef = useRef(null); + const ownerSortColumn = config?.ownerSortColumn; + const displayCards = useMemo(() => { - if (!isExpanded) return filteredRows.slice(0, visibleCardCount); + let source = filteredRows; + + if (ownerSortColumn && currentUserId) { + const mine: RowData[] = []; + const others: RowData[] = []; + for (const row of source) { + if (String(row[ownerSortColumn] ?? "") === currentUserId) { + mine.push(row); + } else { + others.push(row); + } + } + source = [...mine, ...others]; + } + + if (!isExpanded) return source.slice(0, visibleCardCount); const start = (currentPage - 1) * expandedCardsPerPage; - return filteredRows.slice(start, start + expandedCardsPerPage); - }, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage]); + return source.slice(start, start + expandedCardsPerPage); + }, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, currentUserId]); const totalPages = isExpanded ? Math.ceil(filteredRows.length / expandedCardsPerPage) : 1; const needsPagination = isExpanded && totalPages > 1; @@ -756,10 +802,17 @@ export function PopCardListV2Component({ if (firstPending) { firstPending.isCurrent = true; } } - return fetchedRows.map((row) => ({ - ...row, - __processFlow__: processMap.get(String(row.id)) || [], - })); + return fetchedRows.map((row) => { + const steps = processMap.get(String(row.id)) || []; + const current = steps.find((s) => s.isCurrent); + const processFields: Record = {}; + if (current?.rawData) { + for (const [key, val] of Object.entries(current.rawData)) { + processFields[`__process_${key}`] = val; + } + } + return { ...row, __processFlow__: steps, ...processFields }; + }); }, []); const fetchData = useCallback(async () => { @@ -1041,6 +1094,7 @@ export function PopCardListV2Component({ onToggleRowSelect={() => toggleRowSelection(row)} onEnterSelectMode={enterSelectMode} onOpenPopModal={openPopModal} + currentUserId={currentUserId} /> ))} @@ -1148,6 +1202,8 @@ interface CardV2Props { onToggleRowSelect?: () => void; onEnterSelectMode?: (whenStatus: string, buttonConfig: Record) => void; onOpenPopModal?: (screenId: string, row: RowData) => void; + currentUserId?: string; + isLockedByOther?: boolean; } function CardV2({ @@ -1155,7 +1211,7 @@ function CardV2({ parentComponentId, isCartListMode, isSelected, onToggleSelect, onDeleteItem, onUpdateQuantity, onRefresh, selectMode, isSelectModeSelected, isSelectable, onToggleRowSelect, onEnterSelectMode, - onOpenPopModal, + onOpenPopModal, currentUserId, isLockedByOther, }: CardV2Props) { const inputField = config?.inputField; const cartAction = config?.cartAction; @@ -1167,6 +1223,72 @@ function CardV2({ const [packageEntries, setPackageEntries] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); + const [qtyModalState, setQtyModalState] = useState<{ + open: boolean; + row: RowData; + processId?: string | number; + action: ActionButtonClickAction; + } | null>(null); + + const handleQtyConfirm = useCallback(async (value: number) => { + if (!qtyModalState) return; + const { row: actionRow, processId: qtyProcessId, action } = qtyModalState; + setQtyModalState(null); + if (!action.targetTable || !action.updates) return; + + const rowId = qtyProcessId ?? actionRow.id ?? actionRow.pk; + if (!rowId) { toast.error("대상 레코드 ID를 찾을 수 없습니다."); return; } + + const lookupValue = action.joinConfig + ? String(actionRow[action.joinConfig.sourceColumn] ?? rowId) + : rowId; + const lookupColumn = action.joinConfig?.targetColumn || "id"; + + const tasks = action.updates.map((u, idx) => ({ + id: `qty-update-${idx}`, + type: "data-update" as const, + targetTable: action.targetTable!, + targetColumn: u.column, + operationType: (u.operationType || "assign") as "assign" | "add" | "subtract", + valueSource: "fixed" as const, + fixedValue: u.valueType === "userInput" ? String(value) : + u.valueType === "static" ? (u.value ?? "") : + u.valueType === "currentUser" ? "__CURRENT_USER__" : + u.valueType === "currentTime" ? "__CURRENT_TIME__" : + u.valueType === "columnRef" ? String(actionRow[u.value ?? ""] ?? "") : + (u.value ?? ""), + lookupMode: "manual" as const, + manualItemField: lookupColumn, + manualPkColumn: lookupColumn, + ...(idx === 0 && action.preCondition ? { preCondition: action.preCondition } : {}), + })); + + const targetRow = action.joinConfig + ? { ...actionRow, [lookupColumn]: lookupValue } + : qtyProcessId ? { ...actionRow, id: qtyProcessId } : actionRow; + + try { + const result = await apiClient.post("/pop/execute-action", { + tasks, + data: { items: [targetRow], fieldValues: {} }, + mappings: {}, + }); + if (result.data?.success) { + toast.success(result.data.message || "처리 완료"); + onRefresh?.(); + } else { + toast.error(result.data?.message || "처리 실패"); + } + } catch (err: unknown) { + if ((err as any)?.response?.status === 409) { + toast.error((err as any).response?.data?.message || "이미 다른 사용자가 처리한 작업입니다."); + onRefresh?.(); + } else { + toast.error(err instanceof Error ? err.message : "처리 중 오류 발생"); + } + } + }, [qtyModalState, onRefresh]); + const rowKey = keyColumnName && row[keyColumnName] ? String(row[keyColumnName]) : ""; const isCarted = cart.isItemInCart(rowKey); const existingCartItem = cart.getCartItem(rowKey); @@ -1365,7 +1487,11 @@ function CardV2({ } for (const action of actionsToRun) { - if (action.type === "immediate" && action.updates && action.updates.length > 0 && action.targetTable) { + if (action.type === "quantity-input" && action.targetTable && action.updates) { + if (action.confirmMessage && !window.confirm(action.confirmMessage)) return; + setQtyModalState({ open: true, row: actionRow, processId, action }); + return; + } else if (action.type === "immediate" && action.updates && action.updates.length > 0 && action.targetTable) { if (action.confirmMessage) { if (!window.confirm(action.confirmMessage)) return; } @@ -1381,7 +1507,7 @@ function CardV2({ type: "data-update" as const, targetTable: action.targetTable!, targetColumn: u.column, - operationType: "assign" as const, + operationType: (u.operationType || "assign") as "assign" | "add" | "subtract", valueSource: "fixed" as const, fixedValue: u.valueType === "static" ? (u.value ?? "") : u.valueType === "currentUser" ? "__CURRENT_USER__" : @@ -1391,6 +1517,7 @@ function CardV2({ lookupMode: "manual" as const, manualItemField: lookupColumn, manualPkColumn: lookupColumn, + ...(idx === 0 && action.preCondition ? { preCondition: action.preCondition } : {}), })); const targetRow = action.joinConfig ? { ...actionRow, [lookupColumn]: lookupValue } @@ -1408,7 +1535,12 @@ function CardV2({ return; } } catch (err: unknown) { - toast.error(err instanceof Error ? err.message : "처리 중 오류 발생"); + if ((err as any)?.response?.status === 409) { + toast.error((err as any).response?.data?.message || "이미 다른 사용자가 처리한 작업입니다."); + onRefresh?.(); + } else { + toast.error(err instanceof Error ? err.message : "처리 중 오류 발생"); + } return; } } else if (action.type === "modal-open" && action.modalScreenId) { @@ -1418,6 +1550,7 @@ function CardV2({ }, packageEntries, inputUnit: inputField?.unit, + currentUserId, })} ))} @@ -1437,6 +1570,17 @@ function CardV2({ /> )} + {qtyModalState?.open && ( + { if (!open) setQtyModalState(null); }} + unit={qtyModalState.action.quantityInput?.unit || "EA"} + maxValue={calculateMaxQty(qtyModalState.row, qtyModalState.processId, qtyModalState.action.quantityInput)} + showPackageUnit={qtyModalState.action.quantityInput?.enablePackage ?? false} + onConfirm={(value) => handleQtyConfirm(value)} + /> + )} + ); } diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx index 79d8a31e..04ba0622 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx @@ -65,6 +65,33 @@ import { type ColumnInfo, } from "../pop-dashboard/utils/dataFetcher"; +// ===== 컬럼 옵션 그룹 ===== + +interface ColumnOptionGroup { + groupLabel: string; + options: { value: string; label: string }[]; +} + +function renderColumnOptionGroups(groups: ColumnOptionGroup[]) { + if (groups.length <= 1) { + return groups.flatMap((g) => + g.options.map((o) => ( + {o.label} + )) + ); + } + return groups + .filter((g) => g.options.length > 0) + .map((g) => ( + + {g.groupLabel} + {g.options.map((o) => ( + {o.label} + ))} + + )); +} + // ===== Props ===== interface ConfigPanelProps { @@ -271,6 +298,7 @@ export function PopCardListV2ConfigPanel({ config, onUpdate }: ConfigPanelProps) )} @@ -759,10 +787,36 @@ function TabCardDesign({ sourceTable: j.targetTable, })) ); - const allColumnOptions = [ - ...availableColumns.map((c) => ({ value: c.name, label: c.name })), - ...joinedColumns.map((c) => ({ value: c.name, label: `${c.displayName} (${c.sourceTable})` })), + + const [processColumns, setProcessColumns] = useState([]); + const timelineCell = cfg.cardGrid.cells.find((c) => c.type === "timeline" && c.timelineSource?.processTable); + const processTableName = timelineCell?.timelineSource?.processTable || ""; + useEffect(() => { + if (!processTableName) { setProcessColumns([]); return; } + fetchTableColumns(processTableName) + .then(setProcessColumns) + .catch(() => setProcessColumns([])); + }, [processTableName]); + + const columnOptionGroups: ColumnOptionGroup[] = [ + { + groupLabel: `메인 (${cfg.dataSource.tableName || "테이블"})`, + options: availableColumns.map((c) => ({ value: c.name, label: c.name })), + }, + ...(joinedColumns.length > 0 + ? [{ + groupLabel: "조인", + options: joinedColumns.map((c) => ({ value: c.name, label: `${c.displayName} (${c.sourceTable})` })), + }] + : []), + ...(processColumns.length > 0 + ? [{ + groupLabel: `공정 (${processTableName})`, + options: processColumns.map((c) => ({ value: `__process_${c.name}`, label: c.name })), + }] + : []), ]; + const allColumnOptions = columnOptionGroups.flatMap((g) => g.options); const [selectedCellId, setSelectedCellId] = useState(null); const [mergeMode, setMergeMode] = useState(false); @@ -1273,6 +1327,7 @@ function TabCardDesign({ cell={selectedCell} allCells={grid.cells} allColumnOptions={allColumnOptions} + columnOptionGroups={columnOptionGroups} columns={columns} selectedColumns={selectedColumns} tables={tables} @@ -1291,6 +1346,7 @@ function CellDetailEditor({ cell, allCells, allColumnOptions, + columnOptionGroups, columns, selectedColumns, tables, @@ -1301,6 +1357,7 @@ function CellDetailEditor({ cell: CardCellDefinitionV2; allCells: CardCellDefinitionV2[]; allColumnOptions: { value: string; label: string }[]; + columnOptionGroups: ColumnOptionGroup[]; columns: ColumnInfo[]; selectedColumns: string[]; tables: TableInfo[]; @@ -1348,9 +1405,7 @@ function CellDetailEditor({ 미지정 - {allColumnOptions.map((o) => ( - {o.label} - ))} + {renderColumnOptionGroups(columnOptionGroups)} )} @@ -1417,9 +1472,9 @@ function CellDetailEditor({ {/* 타입별 상세 설정 */} {cell.type === "status-badge" && } {cell.type === "timeline" && } - {cell.type === "action-buttons" && } - {cell.type === "footer-status" && } - {cell.type === "field" && } + {cell.type === "action-buttons" && } + {cell.type === "footer-status" && } + {cell.type === "field" && } {cell.type === "number-input" && (
숫자 입력 설정 @@ -1429,7 +1484,7 @@ function CellDetailEditor({ 없음 - {allColumnOptions.map((o) => {o.label})} + {renderColumnOptionGroups(columnOptionGroups)}
@@ -1809,12 +1864,14 @@ function ActionButtonsEditor({ cell, allCells, allColumnOptions, + columnOptionGroups, availableTableOptions, onUpdate, }: { cell: CardCellDefinitionV2; allCells: CardCellDefinitionV2[]; allColumnOptions: { value: string; label: string }[]; + columnOptionGroups: ColumnOptionGroup[]; availableTableOptions: { value: string; label: string }[]; onUpdate: (partial: Partial) => void; }) { @@ -1975,7 +2032,7 @@ function ActionButtonsEditor({ const isSectionOpen = (key: string) => expandedSections[key] !== false; - const ACTION_TYPE_LABELS: Record = { immediate: "즉시 실행", "select-mode": "선택 후 실행", "modal-open": "모달 열기" }; + const ACTION_TYPE_LABELS: Record = { immediate: "즉시 실행", "select-mode": "선택 후 실행", "modal-open": "모달 열기", "quantity-input": "수량 입력" }; const getCondSummary = (btn: ActionButtonDef) => { const c = btn.showCondition; @@ -1985,6 +2042,7 @@ function ActionButtonsEditor({ return opt ? opt.label : (c.value || "미설정"); } if (c.type === "column-value") return `${c.column || "?"} = ${c.value || "?"}`; + if (c.type === "owner-match") return `소유자(${c.column || "?"})`; return "항상"; }; @@ -2081,8 +2139,21 @@ function ActionButtonsEditor({ 항상 타임라인 카드 컬럼 + 소유자 일치 + {condType === "owner-match" && ( + + )} {condType === "timeline-status" && ( 즉시 실행 + 수량 입력 선택 후 실행 모달 열기 @@ -2191,6 +2261,50 @@ function ActionButtonsEditor({ /> )} + {aType === "quantity-input" && ( +
+ addActionUpdate(bi, ai)} + onUpdateUpdate={(ui, p) => updateActionUpdate(bi, ai, ui, p)} + onRemoveUpdate={(ui) => removeActionUpdate(bi, ai, ui)} + onUpdateAction={(p) => updateAction(bi, ai, p)} + /> +
+ 수량 모달 설정 +
+ 최대값 컬럼 + updateAction(bi, ai, { quantityInput: { ...action.quantityInput, maxColumn: e.target.value } })} + placeholder="예: qty" + className="h-6 flex-1 text-[10px]" + /> +
+
+ 현재값 컬럼 + updateAction(bi, ai, { quantityInput: { ...action.quantityInput, currentColumn: e.target.value } })} + placeholder="예: input_qty" + className="h-6 flex-1 text-[10px]" + /> +
+
+ 단위 + updateAction(bi, ai, { quantityInput: { ...action.quantityInput, unit: e.target.value } })} + placeholder="예: EA" + className="h-6 w-20 text-[10px]" + /> +
+
+
+ )} + {aType === "select-mode" && (
@@ -2455,6 +2569,70 @@ function ImmediateActionEditor({ className="h-6 flex-1 text-[10px]" />
+ + {/* 사전 조건 (중복 방지) */} +
+
+ 사전 조건 (중복 방지) + { + if (checked) { + onUpdateAction({ preCondition: { column: "", expectedValue: "", failMessage: "" } }); + } else { + onUpdateAction({ preCondition: undefined }); + } + }} + className="h-3.5 w-7 [&>span]:h-2.5 [&>span]:w-2.5" + /> +
+ {action.preCondition && ( +
+
+ 검증 컬럼 + +
+
+ 기대값 + onUpdateAction({ preCondition: { ...action.preCondition!, expectedValue: e.target.value } })} + placeholder="예: waiting" + className="h-6 flex-1 text-[10px]" + /> +
+
+ 실패 메시지 + onUpdateAction({ preCondition: { ...action.preCondition!, failMessage: e.target.value } })} + placeholder="이미 다른 사용자가 처리했습니다" + className="h-6 flex-1 text-[10px]" + /> +
+

+ 실행 시 해당 컬럼의 현재 DB 값이 기대값과 일치할 때만 처리됩니다 +

+
+ )} +
+
변경할 컬럼{tableName ? ` (${tableName})` : ""} @@ -2491,11 +2669,22 @@ function ImmediateActionEditor({ 직접입력 + 사용자 입력 현재 사용자 현재 시간 컬럼 참조 + {u.valueType === "userInput" && ( + + )} {(u.valueType === "static" || u.valueType === "columnRef") && ( ) => void; }) { const footerStatusMap = cell.footerStatusMap || []; @@ -2644,7 +2835,7 @@ function FooterStatusEditor({ 없음 - {allColumnOptions.map((o) => {o.label})} + {renderColumnOptionGroups(columnOptionGroups)}
@@ -2680,10 +2871,12 @@ function FooterStatusEditor({ function FieldConfigEditor({ cell, allColumnOptions, + columnOptionGroups, onUpdate, }: { cell: CardCellDefinitionV2; allColumnOptions: { value: string; label: string }[]; + columnOptionGroups: ColumnOptionGroup[]; onUpdate: (partial: Partial) => void; }) { const valueType = cell.valueType || "column"; @@ -2706,7 +2899,7 @@ function FieldConfigEditor({ onUpdate({ formulaRight: v })}> - {allColumnOptions.map((o) => {o.label})} + {renderColumnOptionGroups(columnOptionGroups)} )} @@ -2741,16 +2934,61 @@ function FieldConfigEditor({ function TabActions({ cfg, onUpdate, + columns, }: { cfg: PopCardListV2Config; onUpdate: (partial: Partial) => void; + columns: ColumnInfo[]; }) { const overflow = cfg.overflow || { mode: "loadMore" as const, visibleCount: 6 }; const clickAction = cfg.cardClickAction || "none"; const modalConfig = cfg.cardClickModalConfig || { screenId: "" }; + const [processColumns, setProcessColumns] = useState([]); + const timelineCell = cfg.cardGrid?.cells?.find((c) => c.type === "timeline" && c.timelineSource?.processTable); + const processTableName = timelineCell?.timelineSource?.processTable || ""; + useEffect(() => { + if (!processTableName) { setProcessColumns([]); return; } + fetchTableColumns(processTableName) + .then(setProcessColumns) + .catch(() => setProcessColumns([])); + }, [processTableName]); + + const ownerColumnGroups: ColumnOptionGroup[] = useMemo(() => [ + { + groupLabel: `메인 (${cfg.dataSource?.tableName || "테이블"})`, + options: columns.map((c) => ({ value: c.name, label: c.name })), + }, + ...(processColumns.length > 0 + ? [{ + groupLabel: `공정 (${processTableName})`, + options: processColumns.map((c) => ({ value: `__process_${c.name}`, label: c.name })), + }] + : []), + ], [columns, processColumns, processTableName, cfg.dataSource?.tableName]); + return (
+ {/* 소유자 우선 정렬 */} +
+ +
+ +
+

+ 선택한 컬럼 값이 현재 로그인 사용자와 일치하는 카드가 맨 위에 표시됩니다 +

+
+ {/* 카드 선택 시 */}
diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx index f1863b13..180dc219 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx @@ -70,6 +70,7 @@ export interface CellRendererProps { onEnterSelectMode?: (whenStatus: string, buttonConfig: Record) => void; packageEntries?: PackageEntry[]; inputUnit?: string; + currentUserId?: string; } // ===== 메인 디스패치 ===== @@ -592,7 +593,7 @@ function TimelineCell({ cell, row }: CellRendererProps) { // ===== 11. action-buttons ===== -function evaluateShowCondition(btn: ActionButtonDef, row: RowData): "visible" | "disabled" | "hidden" { +function evaluateShowCondition(btn: ActionButtonDef, row: RowData, currentUserId?: string): "visible" | "disabled" | "hidden" { const cond = btn.showCondition; if (!cond || cond.type === "always") return "visible"; @@ -603,6 +604,9 @@ function evaluateShowCondition(btn: ActionButtonDef, row: RowData): "visible" | matched = subStatus !== undefined && String(subStatus) === cond.value; } else if (cond.type === "column-value" && cond.column) { matched = String(row[cond.column] ?? "") === (cond.value ?? ""); + } else if (cond.type === "owner-match" && cond.column) { + const ownerValue = String(row[cond.column] ?? ""); + matched = !!currentUserId && ownerValue === currentUserId; } else { return "visible"; } @@ -611,7 +615,7 @@ function evaluateShowCondition(btn: ActionButtonDef, row: RowData): "visible" | return cond.unmatchBehavior === "disabled" ? "disabled" : "hidden"; } -function ActionButtonsCell({ cell, row, onActionButtonClick, onEnterSelectMode }: CellRendererProps) { +function ActionButtonsCell({ cell, row, onActionButtonClick, onEnterSelectMode, currentUserId }: CellRendererProps) { const processFlow = row.__processFlow__ as { isCurrent: boolean; processId?: string | number }[] | undefined; const currentProcess = processFlow?.find((s) => s.isCurrent); const currentProcessId = currentProcess?.processId; @@ -619,7 +623,7 @@ function ActionButtonsCell({ cell, row, onActionButtonClick, onEnterSelectMode } if (cell.actionButtons && cell.actionButtons.length > 0) { const evaluated = cell.actionButtons.map((btn) => ({ btn, - state: evaluateShowCondition(btn, row), + state: evaluateShowCondition(btn, row, currentUserId), })); const activeBtn = evaluated.find((e) => e.state === "visible"); diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts index 3b7ff73e..3680578e 100644 --- a/frontend/lib/registry/pop-components/types.ts +++ b/frontend/lib/registry/pop-components/types.ts @@ -851,7 +851,8 @@ export interface CardCellDefinitionV2 { export interface ActionButtonUpdate { column: string; value?: string; - valueType: "static" | "currentUser" | "currentTime" | "columnRef"; + valueType: "static" | "currentUser" | "currentTime" | "columnRef" | "userInput"; + operationType?: "assign" | "add" | "subtract"; } // 액션 버튼 클릭 시 동작 모드 @@ -881,34 +882,49 @@ export interface SelectModeConfig { export interface SelectModeButtonConfig { label: string; variant: ButtonVariant; - clickMode: "status-change" | "modal-open" | "cancel-select"; + clickMode: "status-change" | "modal-open" | "cancel-select" | "quantity-input"; targetTable?: string; updates?: ActionButtonUpdate[]; confirmMessage?: string; modalScreenId?: string; + quantityInput?: QuantityInputConfig; } // ===== 버튼 중심 구조 (신규) ===== export interface ActionButtonShowCondition { - type: "timeline-status" | "column-value" | "always"; + type: "timeline-status" | "column-value" | "always" | "owner-match"; value?: string; column?: string; unmatchBehavior?: "hidden" | "disabled"; } export interface ActionButtonClickAction { - type: "immediate" | "select-mode" | "modal-open"; + type: "immediate" | "select-mode" | "modal-open" | "quantity-input"; targetTable?: string; updates?: ActionButtonUpdate[]; confirmMessage?: string; selectModeButtons?: SelectModeButtonConfig[]; modalScreenId?: string; - // 외부 테이블 조인 설정 (DB 직접 선택 시) joinConfig?: { - sourceColumn: string; // 메인 테이블의 FK 컬럼 - targetColumn: string; // 외부 테이블의 매칭 컬럼 + sourceColumn: string; + targetColumn: string; }; + quantityInput?: QuantityInputConfig; + preCondition?: ActionPreCondition; +} + +export interface QuantityInputConfig { + maxColumn?: string; + currentColumn?: string; + unit?: string; + enablePackage?: boolean; +} + +export interface ActionPreCondition { + column: string; + expectedValue: string; + failMessage?: string; } export interface ActionButtonDef { @@ -976,6 +992,7 @@ export interface PopCardListV2Config { cartAction?: CardCartActionConfig; cartListMode?: CartListModeConfig; saveMapping?: CardListSaveMapping; + ownerSortColumn?: string; } /** 카드 컴포넌트가 하위 필터 적용 시 주입하는 가상 컬럼 키 */ From c067c373906bfbc4da9079e9e703dbe0f5f38ba8 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 13 Mar 2026 14:19:54 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20BLOCK=20DETAIL=20Phase=202=20-=20?= =?UTF-8?q?=EC=83=9D=EC=82=B0=20=EA=B3=B5=EC=A0=95=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20API=20=ED=8C=8C=EC=9D=B4=ED=94=84?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EC=9E=91=EC=97=85=EC=A7=80=EC=8B=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EA=B3=B5=EC=A0=95+=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=9D=BC=EA=B4=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B3=BC=20=EA=B3=B5=EC=A0=95=EB=B3=84=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=A0=9C=EC=96=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=B0=B1=EC=97=94=EB=93=9C=20API=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=EC=B6=95=ED=95=9C=EB=8B=A4.=20[=EC=8B=A0=EA=B7=9C]=20?= =?UTF-8?q?popProductionController.ts=20-=20createWorkProcesses:=20POST=20?= =?UTF-8?q?/api/pop/production/create-work-processes=20=20=20-=20item=5Fro?= =?UTF-8?q?uting=5Fdetail=20+=20process=5Fmng=20JOIN=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=B5=EC=A0=95=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=20=20-=20work=5Forder=5Fprocess=20INSERT=20(=EA=B3=B5=EC=A0=95?= =?UTF-8?q?=EB=B3=84)=20=20=20-=20process=5Fwork=5Fresult=20INSERT=20SELEC?= =?UTF-8?q?T=20(=EB=A7=88=EC=8A=A4=ED=84=B0=20=EC=8A=A4=EB=83=85=EC=83=B7?= =?UTF-8?q?=20=EB=B3=B5=EC=82=AC)=20=20=20-=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80=20(409=20Conflict)=20?= =?UTF-8?q?=20=20-=201=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20-=20controlTimer:=20POST=20/api/pop/production/time?= =?UTF-8?q?r=20=20=20-=20start:=20started=5Fat=20=EC=84=A4=EC=A0=95=20+=20?= =?UTF-8?q?status=20waiting->in=5Fprogress=20(=EB=A9=B1=EB=93=B1)=20=20=20?= =?UTF-8?q?-=20pause:=20paused=5Fat=20=EC=84=A4=EC=A0=95=20=20=20-=20resum?= =?UTF-8?q?e:=20total=5Fpaused=5Ftime=20=EB=88=84=EC=A0=81=20+=20paused=5F?= =?UTF-8?q?at=20=EC=B4=88=EA=B8=B0=ED=99=94=20[=EC=8B=A0=EA=B7=9C]=20popPr?= =?UTF-8?q?oductionRoutes.ts=20-=20authenticateToken=20=EB=AF=B8=EB=93=A4?= =?UTF-8?q?=EC=9B=A8=EC=96=B4=20=EC=A0=84=EC=97=AD=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?-=202=EA=B0=9C=20POST=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20[=EC=88=98=EC=A0=95]=20app.ts?= =?UTF-8?q?=20-=20popProductionRoutes=20import=20+=20/api/pop/production?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/app.ts | 2 + .../controllers/popProductionController.ts | 291 ++++++++++++++++++ .../src/routes/popProductionRoutes.ts | 15 + 3 files changed, 308 insertions(+) create mode 100644 backend-node/src/controllers/popProductionController.ts create mode 100644 backend-node/src/routes/popProductionRoutes.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index f45a88cd..6b86a333 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -124,6 +124,7 @@ import entitySearchRoutes, { import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면 임베딩 및 데이터 전달 import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리 import popActionRoutes from "./routes/popActionRoutes"; // POP 액션 실행 +import popProductionRoutes from "./routes/popProductionRoutes"; // POP 생산 관리 (공정 생성/타이머) import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리 import approvalRoutes from "./routes/approvalRoutes"; // 결재 시스템 import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리 @@ -259,6 +260,7 @@ app.use("/api/table-management", entityJoinRoutes); // 🎯 Entity 조인 기능 app.use("/api/screen-management", screenManagementRoutes); app.use("/api/screen-groups", screenGroupRoutes); // 화면 그룹 관리 app.use("/api/pop", popActionRoutes); // POP 액션 실행 +app.use("/api/pop/production", popProductionRoutes); // POP 생산 관리 app.use("/api/common-codes", commonCodeRoutes); app.use("/api/dynamic-form", dynamicFormRoutes); app.use("/api/files", fileRoutes); diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts new file mode 100644 index 00000000..d575b07a --- /dev/null +++ b/backend-node/src/controllers/popProductionController.ts @@ -0,0 +1,291 @@ +import { Response } from "express"; +import { getPool } from "../database/db"; +import logger from "../utils/logger"; +import { AuthenticatedRequest } from "../middleware/authMiddleware"; + +/** + * D-BE1: 작업지시 공정 일괄 생성 + * PC에서 작업지시 생성 후 호출. 1 트랜잭션으로 work_order_process + process_work_result 일괄 생성. + */ +export const createWorkProcesses = async ( + req: AuthenticatedRequest, + res: Response +) => { + const pool = getPool(); + const client = await pool.connect(); + + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + + const { work_instruction_id, item_code, routing_version_id, plan_qty } = + req.body; + + if (!work_instruction_id || !routing_version_id) { + return res.status(400).json({ + success: false, + message: + "work_instruction_id와 routing_version_id는 필수입니다.", + }); + } + + logger.info("[pop/production] create-work-processes 요청", { + companyCode, + userId, + work_instruction_id, + item_code, + routing_version_id, + plan_qty, + }); + + await client.query("BEGIN"); + + // 중복 호출 방지: 이미 생성된 공정이 있는지 확인 + const existCheck = await client.query( + `SELECT COUNT(*) as cnt FROM work_order_process + WHERE wo_id = $1 AND company_code = $2`, + [work_instruction_id, companyCode] + ); + if (parseInt(existCheck.rows[0].cnt, 10) > 0) { + await client.query("ROLLBACK"); + return res.status(409).json({ + success: false, + message: "이미 공정이 생성된 작업지시입니다.", + }); + } + + // 1. item_routing_detail + process_mng JOIN (공정 목록 + 공정명) + const routingDetails = await client.query( + `SELECT rd.id, rd.seq_no, rd.process_code, + COALESCE(pm.process_name, rd.process_code) as process_name, + rd.is_required, rd.is_fixed_order, rd.standard_time + FROM item_routing_detail rd + LEFT JOIN process_mng pm ON pm.process_code = rd.process_code + AND pm.company_code = rd.company_code + WHERE rd.routing_version_id = $1 AND rd.company_code = $2 + ORDER BY CAST(rd.seq_no AS int) NULLS LAST`, + [routing_version_id, companyCode] + ); + + if (routingDetails.rows.length === 0) { + await client.query("ROLLBACK"); + return res.status(404).json({ + success: false, + message: "라우팅 버전에 등록된 공정이 없습니다.", + }); + } + + const processes: Array<{ + id: string; + seq_no: string; + process_name: string; + checklist_count: number; + }> = []; + let totalChecklists = 0; + + for (const rd of routingDetails.rows) { + // 2. work_order_process INSERT + const wopResult = await client.query( + `INSERT INTO work_order_process ( + company_code, wo_id, seq_no, process_code, process_name, + is_required, is_fixed_order, standard_time, plan_qty, + status, routing_detail_id, writer + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING id`, + [ + companyCode, + work_instruction_id, + rd.seq_no, + rd.process_code, + rd.process_name, + rd.is_required, + rd.is_fixed_order, + rd.standard_time, + plan_qty || null, + "waiting", + rd.id, + userId, + ] + ); + const wopId = wopResult.rows[0].id; + + // 3. process_work_result INSERT (스냅샷 복사) + // process_work_item + process_work_item_detail에서 해당 routing_detail의 항목 조회 후 복사 + const snapshotResult = await client.query( + `INSERT INTO process_work_result ( + company_code, work_order_process_id, + source_work_item_id, source_detail_id, + work_phase, item_title, item_sort_order, + detail_content, detail_type, detail_sort_order, is_required, + inspection_code, inspection_method, unit, lower_limit, upper_limit, + input_type, lookup_target, display_fields, duration_minutes, + status, writer + ) + SELECT + pwi.company_code, $1, + pwi.id, pwd.id, + pwi.work_phase, pwi.title, pwi.sort_order::text, + pwd.content, pwd.detail_type, pwd.sort_order::text, pwd.is_required, + pwd.inspection_code, pwd.inspection_method, pwd.unit, pwd.lower_limit, pwd.upper_limit, + pwd.input_type, pwd.lookup_target, pwd.display_fields, pwd.duration_minutes::text, + 'pending', $2 + FROM process_work_item pwi + JOIN process_work_item_detail pwd ON pwd.work_item_id = pwi.id + AND pwd.company_code = pwi.company_code + WHERE pwi.routing_detail_id = $3 + AND pwi.company_code = $4 + ORDER BY pwi.sort_order, pwd.sort_order`, + [wopId, userId, rd.id, companyCode] + ); + + const checklistCount = snapshotResult.rowCount ?? 0; + totalChecklists += checklistCount; + + processes.push({ + id: wopId, + seq_no: rd.seq_no, + process_name: rd.process_name, + checklist_count: checklistCount, + }); + + logger.info("[pop/production] 공정 생성 완료", { + wopId, + processName: rd.process_name, + checklistCount, + }); + } + + await client.query("COMMIT"); + + logger.info("[pop/production] create-work-processes 완료", { + companyCode, + work_instruction_id, + total_processes: processes.length, + total_checklists: totalChecklists, + }); + + return res.json({ + success: true, + data: { + processes, + total_processes: processes.length, + total_checklists: totalChecklists, + }, + }); + } catch (error: any) { + await client.query("ROLLBACK"); + logger.error("[pop/production] create-work-processes 오류:", error); + return res.status(500).json({ + success: false, + message: error.message || "공정 생성 중 오류가 발생했습니다.", + }); + } finally { + client.release(); + } +}; + +/** + * D-BE2: 타이머 API (시작/일시정지/재시작) + */ +export const controlTimer = async ( + req: AuthenticatedRequest, + res: Response +) => { + const pool = getPool(); + + try { + const companyCode = req.user!.companyCode; + const userId = req.user!.userId; + + const { work_order_process_id, action } = req.body; + + if (!work_order_process_id || !action) { + return res.status(400).json({ + success: false, + message: "work_order_process_id와 action은 필수입니다.", + }); + } + + if (!["start", "pause", "resume"].includes(action)) { + return res.status(400).json({ + success: false, + message: "action은 start, pause, resume 중 하나여야 합니다.", + }); + } + + logger.info("[pop/production] timer 요청", { + companyCode, + userId, + work_order_process_id, + action, + }); + + let result; + + switch (action) { + case "start": + // 최초 1회만 설정, 이미 있으면 무시 + result = await pool.query( + `UPDATE work_order_process + SET started_at = CASE WHEN started_at IS NULL THEN NOW()::text ELSE started_at END, + status = CASE WHEN status = 'waiting' THEN 'in_progress' ELSE status END, + updated_date = NOW() + WHERE id = $1 AND company_code = $2 + RETURNING id, started_at, status`, + [work_order_process_id, companyCode] + ); + break; + + case "pause": + result = await pool.query( + `UPDATE work_order_process + SET paused_at = NOW()::text, + updated_date = NOW() + WHERE id = $1 AND company_code = $2 AND paused_at IS NULL + RETURNING id, paused_at`, + [work_order_process_id, companyCode] + ); + break; + + case "resume": + // 일시정지 시간 누적 후 paused_at 초기화 + result = await pool.query( + `UPDATE work_order_process + SET total_paused_time = ( + COALESCE(total_paused_time::int, 0) + + EXTRACT(EPOCH FROM NOW() - paused_at::timestamp)::int + )::text, + paused_at = NULL, + updated_date = NOW() + WHERE id = $1 AND company_code = $2 AND paused_at IS NOT NULL + RETURNING id, total_paused_time`, + [work_order_process_id, companyCode] + ); + break; + } + + if (!result || result.rowCount === 0) { + return res.status(404).json({ + success: false, + message: "대상 공정을 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.", + }); + } + + logger.info("[pop/production] timer 완료", { + action, + work_order_process_id, + result: result.rows[0], + }); + + return res.json({ + success: true, + data: result.rows[0], + }); + } catch (error: any) { + logger.error("[pop/production] timer 오류:", error); + return res.status(500).json({ + success: false, + message: error.message || "타이머 처리 중 오류가 발생했습니다.", + }); + } +}; diff --git a/backend-node/src/routes/popProductionRoutes.ts b/backend-node/src/routes/popProductionRoutes.ts new file mode 100644 index 00000000..f20d470d --- /dev/null +++ b/backend-node/src/routes/popProductionRoutes.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { + createWorkProcesses, + controlTimer, +} from "../controllers/popProductionController"; + +const router = Router(); + +router.use(authenticateToken); + +router.post("/create-work-processes", createWorkProcesses); +router.post("/timer", controlTimer); + +export default router; From c4d7b165382609d11c81456d43f1ecad41a2b1c8 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 13 Mar 2026 14:23:26 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix:=20LOCK-OWNER=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20UI=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=B6=84=20=EB=B0=98=EC=98=81=20a2c532c7=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=88=84=EB=9D=BD=EB=90=9C=20CardV2=20?= =?UTF-8?q?=EC=9E=A0=EA=B8=88=20UI=EB=A5=BC=20=EB=B0=98=EC=98=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20-=20locked=20=EA=B3=84=EC=82=B0:=20ownerSortColumn?= =?UTF-8?q?=20=EA=B0=92=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=99=80=20?= =?UTF-8?q?=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=8B=9C=20true=20-=20isLockedBy?= =?UTF-8?q?Other=20prop=EC=9D=84=20CardV2=EC=97=90=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?-=20=EC=9E=A0=EA=B8=88=20=EC=B9=B4=EB=93=9C:=20opacity-50,=20cu?= =?UTF-8?q?rsor-not-allowed,=20onClick/onKeyDown=20=EC=B0=A8=EB=8B=A8,=20t?= =?UTF-8?q?abIndex=3D-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopCardListV2Component.tsx | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 3a52a36e..141c3ffc 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -1067,36 +1067,42 @@ export function PopCardListV2Component({ className={`min-h-0 flex-1 grid ${scrollClassName}`} style={{ ...cardAreaStyle, alignContent: "start", justifyContent: isHorizontalMode ? "start" : "center" }} > - {displayCards.map((row, index) => ( - { - const cartId = row.__cart_id != null ? String(row.__cart_id) : ""; - if (!cartId) return; - setSelectedKeys((prev) => { const next = new Set(prev); if (next.has(cartId)) next.delete(cartId); else next.add(cartId); return next; }); - }} - onDeleteItem={handleDeleteItem} - onUpdateQuantity={handleUpdateQuantity} - onRefresh={fetchData} - selectMode={selectMode} - isSelectModeSelected={selectedRowIds.has(String(row.id ?? row.pk ?? ""))} - isSelectable={isRowSelectable(row)} - onToggleRowSelect={() => toggleRowSelection(row)} - onEnterSelectMode={enterSelectMode} - onOpenPopModal={openPopModal} - currentUserId={currentUserId} - /> - ))} + {displayCards.map((row, index) => { + const locked = !!ownerSortColumn + && !!String(row[ownerSortColumn] ?? "") + && String(row[ownerSortColumn] ?? "") !== (currentUserId ?? ""); + return ( + { + const cartId = row.__cart_id != null ? String(row.__cart_id) : ""; + if (!cartId) return; + setSelectedKeys((prev) => { const next = new Set(prev); if (next.has(cartId)) next.delete(cartId); else next.add(cartId); return next; }); + }} + onDeleteItem={handleDeleteItem} + onUpdateQuantity={handleUpdateQuantity} + onRefresh={fetchData} + selectMode={selectMode} + isSelectModeSelected={selectedRowIds.has(String(row.id ?? row.pk ?? ""))} + isSelectable={isRowSelectable(row)} + onToggleRowSelect={() => toggleRowSelection(row)} + onEnterSelectMode={enterSelectMode} + onOpenPopModal={openPopModal} + currentUserId={currentUserId} + isLockedByOther={locked} + /> + ); + })}
{/* 선택 모드 하단 액션 바 */} @@ -1394,16 +1400,24 @@ function CardV2({ return (
{ + if (isLockedByOther) return; if (selectMode && isSelectable) { onToggleRowSelect?.(); return; } if (!selectMode) onSelect?.(row); }} role="button" - tabIndex={0} + tabIndex={isLockedByOther ? -1 : 0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { + if (isLockedByOther) return; if (selectMode && isSelectable) { onToggleRowSelect?.(); return; } if (!selectMode) onSelect?.(row); } From 5e8572954a089343feff04a8aea78d5facbd2285 Mon Sep 17 00:00:00 2001 From: kmh Date: Fri, 13 Mar 2026 14:57:07 +0900 Subject: [PATCH 06/18] chore: update .gitignore and remove unused images - Added support for ignoring PNG files in .gitignore to streamline file management. - Deleted unused image files from the .playwright-mcp directory to reduce clutter and improve project organization. - Enhanced column visibility handling in TableListComponent to include width adjustments and localStorage synchronization for better user experience. Made-with: Cursor --- .gitignore | 1 + .playwright-mcp/pivotgrid-demo.png | Bin 336690 -> 0 bytes .playwright-mcp/pivotgrid-table.png | Bin 349840 -> 0 bytes .playwright-mcp/pop-page-initial.png | Bin 25260 -> 0 bytes .../screen/RealtimePreviewDynamic.tsx | 2 +- .../table-list/TableListComponent.tsx | 23 +++++++++++++++++- .../ButtonPrimaryComponent.tsx | 2 ++ .../v2-table-list/TableListComponent.tsx | 23 +++++++++++++++++- 8 files changed, 48 insertions(+), 3 deletions(-) delete mode 100644 .playwright-mcp/pivotgrid-demo.png delete mode 100644 .playwright-mcp/pivotgrid-table.png delete mode 100644 .playwright-mcp/pop-page-initial.png diff --git a/.gitignore b/.gitignore index 5e66bd12..552d1265 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,7 @@ backend-node/uploads/ uploads/ *.jpg *.jpeg +*.png *.gif *.pdf *.doc diff --git a/.playwright-mcp/pivotgrid-demo.png b/.playwright-mcp/pivotgrid-demo.png deleted file mode 100644 index 0fad6fa629368fe2eb3b911756c10337a3bd4b11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 336690 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 z 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

M&A?q$Q>ONRc1A4Y3z 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®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 diff --git a/.playwright-mcp/pivotgrid-table.png b/.playwright-mcp/pivotgrid-table.png deleted file mode 100644 index 79041f47cf7d28b1770197278f8fff5f591bc227..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349840 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

`~yg?uM#JT?4fcocnlR2Rh24ek*Rzy$ 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 = ({ const needsStripBorder = isV2HorizLabel || isButtonComponent; const safeComponentStyle = needsStripBorder ? (() => { - const { borderWidth, borderColor, borderStyle, border, borderRadius, ...rest } = componentStyle as any; + const { borderWidth, borderColor, borderStyle, border, ...rest } = componentStyle as any; return rest; })() : componentStyle; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index babec53c..119cca53 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -379,12 +379,33 @@ export const TableListComponent: React.FC = ({ } }, [tableConfig.selectedTable, currentUserId]); - // columnVisibility 변경 시 컬럼 순서 및 가시성 적용 + // columnVisibility 변경 시 컬럼 순서, 가시성, 너비 적용 useEffect(() => { if (columnVisibility.length > 0) { const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 제외 setColumnOrder(newOrder); + // 너비 적용 + const newWidths: Record = {}; + columnVisibility.forEach((cv) => { + if (cv.width) { + newWidths[cv.columnName] = cv.width; + } + }); + if (Object.keys(newWidths).length > 0) { + setColumnWidths((prev) => ({ ...prev, ...newWidths })); + + // table_column_widths_* localStorage도 동기화 (초기 너비 로드 시 올바른 값 사용) + if (tableConfig.selectedTable && userId) { + const widthsKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; + try { + const existing = localStorage.getItem(widthsKey); + const merged = existing ? { ...JSON.parse(existing), ...newWidths } : newWidths; + localStorage.setItem(widthsKey, JSON.stringify(merged)); + } catch { /* ignore */ } + } + } + // localStorage에 저장 (사용자별) if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index fa0cfaae..e287d512 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -570,6 +570,8 @@ export const ButtonPrimaryComponent: React.FC = ({ ...restComponentStyle, width: "100%", height: "100%", + borderRadius: _br || "0.5rem", + overflow: "hidden", }; // 디자인 모드 스타일 diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 6b8c674c..f7a7c7a1 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -520,12 +520,33 @@ export const TableListComponent: React.FC = ({ } }, [tableConfig.selectedTable, currentUserId]); - // columnVisibility 변경 시 컬럼 순서 및 가시성 적용 + // columnVisibility 변경 시 컬럼 순서, 가시성, 너비 적용 useEffect(() => { if (columnVisibility.length > 0) { const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 제외 setColumnOrder(newOrder); + // 너비 적용 + const newWidths: Record = {}; + columnVisibility.forEach((cv) => { + if (cv.width) { + newWidths[cv.columnName] = cv.width; + } + }); + if (Object.keys(newWidths).length > 0) { + setColumnWidths((prev) => ({ ...prev, ...newWidths })); + + // table_column_widths_* localStorage도 동기화 (초기 너비 로드 시 올바른 값 사용) + if (tableConfig.selectedTable && userId) { + const widthsKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; + try { + const existing = localStorage.getItem(widthsKey); + const merged = existing ? { ...JSON.parse(existing), ...newWidths } : newWidths; + localStorage.setItem(widthsKey, JSON.stringify(merged)); + } catch { /* ignore */ } + } + } + // localStorage에 저장 (사용자별) if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; From 842ac27d60c3ec11827a25e398021a04482e47a5 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 13 Mar 2026 16:03:24 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20V6=20=EC=A0=95=EC=82=AC=EA=B0=81?= =?UTF-8?q?=ED=98=95=20=EB=B8=94=EB=A1=9D=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=8B=A4=ED=97=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EA=B3=A0=EC=A0=95=20=EC=B9=B8=20=EC=88=98(4/6/8/12?= =?UTF-8?q?)=20=EA=B8=B0=EB=B0=98=EC=9D=98=20V5=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=2024px=20=EC=A0=95=EC=82=AC=EA=B0=81?= =?UTF-8?q?=ED=98=95=20=EB=B8=94=EB=A1=9D=20=EA=B8=B0=EB=B0=98=EC=9D=98=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=B9=B8=20=EC=88=98=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20=EB=B7=B0=ED=8F=AC=ED=8A=B8=20=EB=84=88=EB=B9=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B8=94=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EA=B0=80=20=EC=9E=90=EB=8F=99=20=EA=B3=84=EC=82=B0=EB=90=98?= =?UTF-8?q?=EB=A9=B0(375px=3D13=EC=B9=B8,=201024px=3D38=EC=B9=B8),=20?= =?UTF-8?q?=EC=9E=91=EC=9D=80=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8A=94=20=ED=96=89=20=EA=B7=B8=EB=A3=B9=20=EB=A6=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20(CSS=20Flexbox=20wrap=20=EC=9B=90=EB=A6=AC?= =?UTF-8?q?)=EB=A1=9C=20=EC=9E=90=EB=8F=99=20=EC=9E=AC=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=EB=90=9C=EB=8B=A4.=20[=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=BD=94?= =?UTF-8?q?=EC=96=B4]=20-=20pop-layout.ts:=20BLOCK=5FSIZE=3D24,=20BLOCK=5F?= =?UTF-8?q?GAP=3D2,=20BLOCK=5FPADDING=3D8,=20=20=20getBlockColumns()=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=B9=B8=20=EC=88=98=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?,=20GRID=5FBREAKPOINTS=20V6=20=EA=B0=92=20-=20gridUtils.ts:=20?= =?UTF-8?q?=ED=96=89=20=EA=B7=B8=EB=A3=B9=20=EB=A6=AC=ED=94=8C=EB=A1=9C?= =?UTF-8?q?=EC=9A=B0(=EB=B0=A9=EC=8B=9D=20F)=20-=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=ED=96=89=20=EB=AC=B6=EC=9D=8C=20=EC=B2=98=EB=A6=AC,=20=20=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=202x2=EC=B9=B8=20=ED=84=B0=EC=B9=98=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=A5,=20=EB=A9=94=EC=9D=B8=20=EC=BB=A8=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EC=A0=84=EC=B2=B4=20=EB=84=88=EB=B9=84=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20-=20PopRenderer.tsx:=20repeat(N,=201fr)=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=20=EB=A0=8C=EB=8D=94=EB=A7=81,=20=EB=8F=99=EC=A0=81?= =?UTF-8?q?=20=EC=B9=B8=20=EC=88=98=20-=20PopCanvas.tsx:=20=EB=B7=B0?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=ED=94=84=EB=A6=AC=EC=85=8B=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EC=B9=B8=20=EC=88=98,=20=EB=B8=94=EB=A1=9D=20?= =?UTF-8?q?=EC=A2=8C=ED=91=9C=20=EB=B3=80=ED=99=98=20[V5=E2=86=92V6=20?= =?UTF-8?q?=EB=9F=B0=ED=83=80=EC=9E=84=20=EB=B3=80=ED=99=98]=20-=20convert?= =?UTF-8?q?V5LayoutToV6:=20DB=20=EB=AF=B8=EC=88=98=EC=A0=95,=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=20=2012=EC=B9=B8=20=EC=A2=8C=ED=91=9C=20=E2=86=92?= =?UTF-8?q?=2038=EC=B9=B8=20=EB=B8=94=EB=A1=9D=20=EB=B3=80=ED=99=98,=20V5?= =?UTF-8?q?=20overrides=20=EC=A0=9C=EA=B1=B0=20-=20PopDesigner/page.tsx:?= =?UTF-8?q?=20=EB=A1=9C=EB=93=9C=20=EC=A7=80=EC=A0=90=EC=97=90=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=ED=95=A8=EC=88=98=20=EC=82=BD=EC=9E=85=20[?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0]=20-=20ComponentEditorP?= =?UTF-8?q?anel:=20=EB=86=92=EC=9D=B4=20=ED=91=9C=EC=8B=9C/=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=9D=BC=EB=B2=A8=20V6=20=EC=88=98=EC=B9=98=20-=20?= =?UTF-8?q?PopCardListConfig:=20=EC=B9=B4=EB=93=9C=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?threshold=20V6=20=EA=B8=B0=EC=A4=80=20-=20PopDesigner:=20handle?= =?UTF-8?q?HideComponent=20=EA=B8=B0=EB=B3=B8=20=EB=AA=A8=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=ED=95=B4=EC=A0=9C=20[=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EC=82=AC=EC=9D=B4=EC=A6=88]=20-=20=EC=86=8C=ED=98=95(2x2):?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98,=20=ED=94=84=EB=A1=9C=ED=95=84,?= =?UTF-8?q?=20=EC=8A=A4=EC=BA=90=EB=84=88=20-=20=EC=A4=91=ED=98=95(8x4):?= =?UTF-8?q?=20=EA=B2=80=EC=83=89,=20=EB=B2=84=ED=8A=BC,=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20-=20=EB=8C=80=ED=98=95(19x6~10):=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C,=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C,=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20DB=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4,=20?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4,?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=200=EA=B1=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(pop)/pop/screens/[screenId]/page.tsx | 15 +- .../components/pop/designer/PopCanvas.tsx | 52 +-- .../components/pop/designer/PopDesigner.tsx | 12 +- .../designer/panels/ComponentEditorPanel.tsx | 12 +- .../pop/designer/renderers/PopRenderer.tsx | 47 ++- .../pop/designer/types/pop-layout.ts | 148 +++---- .../pop/designer/utils/gridUtils.ts | 377 ++++++++++-------- .../pop-card-list/PopCardListConfig.tsx | 4 +- 8 files changed, 375 insertions(+), 292 deletions(-) diff --git a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx index 7fe11270..bf2878a5 100644 --- a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx +++ b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx @@ -23,8 +23,11 @@ import { createEmptyPopLayoutV5, GAP_PRESETS, GRID_BREAKPOINTS, + BLOCK_GAP, + BLOCK_PADDING, detectGridMode, } from "@/components/pop/designer/types/pop-layout"; +import { convertV5LayoutToV6 } from "@/components/pop/designer/utils/gridUtils"; // POP 컴포넌트 자동 등록 (레지스트리 초기화 - PopRenderer보다 먼저 import) import "@/lib/registry/pop-components"; import PopViewerWithModals from "@/components/pop/viewer/PopViewerWithModals"; @@ -117,8 +120,8 @@ function PopScreenViewPage() { const popLayout = await screenApi.getLayoutPop(screenId); if (popLayout && isV5Layout(popLayout)) { - // v5 레이아웃 로드 - setLayout(popLayout); + const v6Layout = convertV5LayoutToV6(popLayout); + setLayout(v6Layout); const componentCount = Object.keys(popLayout.components).length; console.log(`[POP] v5 레이아웃 로드됨: ${componentCount}개 컴포넌트`); } else if (popLayout) { @@ -318,12 +321,8 @@ function PopScreenViewPage() { style={{ maxWidth: 1366 }} > {(() => { - // Gap 프리셋 계산 - const currentGapPreset = layout.settings.gapPreset || "medium"; - const gapMultiplier = GAP_PRESETS[currentGapPreset]?.multiplier || 1.0; - const breakpoint = GRID_BREAKPOINTS[currentModeKey]; - const adjustedGap = Math.round(breakpoint.gap * gapMultiplier); - const adjustedPadding = Math.max(8, Math.round(breakpoint.padding * gapMultiplier)); + const adjustedGap = BLOCK_GAP; + const adjustedPadding = BLOCK_PADDING; return ( (null); const canvasRef = useRef(null); - // 현재 뷰포트 해상도 + // V6: 뷰포트에서 동적 블록 칸 수 계산 const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === currentMode)!; - const breakpoint = GRID_BREAKPOINTS[currentMode]; + const dynamicColumns = getBlockColumns(customWidth); + const breakpoint = { + ...GRID_BREAKPOINTS[currentMode], + columns: dynamicColumns, + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + label: `${dynamicColumns}칸 블록`, + }; - // Gap 프리셋 적용 + // V6: 블록 간격 고정 (프리셋 무관) const currentGapPreset = layout.settings.gapPreset || "medium"; - const gapMultiplier = GAP_PRESETS[currentGapPreset]?.multiplier || 1.0; - const adjustedGap = Math.round(breakpoint.gap * gapMultiplier); - const adjustedPadding = Math.max(8, Math.round(breakpoint.padding * gapMultiplier)); + const adjustedGap = BLOCK_GAP; + const adjustedPadding = BLOCK_PADDING; // 숨김 컴포넌트 ID 목록 (activeLayout 기반) const hiddenComponentIds = activeLayout.overrides?.[currentMode]?.hidden || []; @@ -805,7 +807,7 @@ export default function PopCanvas({ {/* 하단 정보 */}
- {breakpoint.label} - {breakpoint.columns}칸 그리드 (행 높이: {breakpoint.rowHeight}px) + V6 블록 그리드 - {dynamicColumns}칸 (블록: {BLOCK_SIZE}px, 간격: {BLOCK_GAP}px)
Space + 드래그: 패닝 | Ctrl + 휠: 줌 diff --git a/frontend/components/pop/designer/PopDesigner.tsx b/frontend/components/pop/designer/PopDesigner.tsx index 36241817..de131032 100644 --- a/frontend/components/pop/designer/PopDesigner.tsx +++ b/frontend/components/pop/designer/PopDesigner.tsx @@ -33,7 +33,7 @@ import { PopModalDefinition, PopDataConnection, } from "./types/pop-layout"; -import { getAllEffectivePositions } from "./utils/gridUtils"; +import { getAllEffectivePositions, convertV5LayoutToV6 } from "./utils/gridUtils"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; import { PopDesignerContext } from "./PopDesignerContext"; @@ -151,13 +151,12 @@ export default function PopDesigner({ const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId); if (loadedLayout && isV5Layout(loadedLayout) && loadedLayout.components && Object.keys(loadedLayout.components).length > 0) { - // v5 레이아웃 로드 - // 기존 레이아웃 호환성: gapPreset이 없으면 기본값 추가 if (!loadedLayout.settings.gapPreset) { loadedLayout.settings.gapPreset = "medium"; } - setLayout(loadedLayout); - setHistory([loadedLayout]); + const v6Layout = convertV5LayoutToV6(loadedLayout); + setLayout(v6Layout); + setHistory([v6Layout]); setHistoryIndex(0); // 기존 컴포넌트 ID에서 최대 숫자 추출하여 idCounter 설정 (중복 방지) @@ -605,9 +604,6 @@ export default function PopDesigner({ // ======================================== const handleHideComponent = useCallback((componentId: string) => { - // 12칸 모드에서는 숨기기 불가 - if (currentMode === "tablet_landscape") return; - const currentHidden = layout.overrides?.[currentMode]?.hidden || []; // 이미 숨겨져 있으면 무시 diff --git a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx index 32ff5e06..ec22426d 100644 --- a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx +++ b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx @@ -7,6 +7,8 @@ import { PopGridPosition, GridMode, GRID_BREAKPOINTS, + BLOCK_SIZE, + getBlockColumns, } from "../types/pop-layout"; import { Settings, @@ -378,7 +380,7 @@ function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate

- 높이: {position.rowSpan * GRID_BREAKPOINTS[currentMode].rowHeight}px + 높이: {position.rowSpan * BLOCK_SIZE + (position.rowSpan - 1) * 2}px

@@ -470,10 +472,10 @@ interface VisibilityFormProps { function VisibilityForm({ component, onUpdate }: VisibilityFormProps) { const modes: Array<{ key: GridMode; label: string }> = [ - { key: "tablet_landscape", label: "태블릿 가로 (12칸)" }, - { key: "tablet_portrait", label: "태블릿 세로 (8칸)" }, - { key: "mobile_landscape", label: "모바일 가로 (6칸)" }, - { key: "mobile_portrait", label: "모바일 세로 (4칸)" }, + { key: "tablet_landscape", label: `태블릿 가로 (${getBlockColumns(1024)}칸)` }, + { key: "tablet_portrait", label: `태블릿 세로 (${getBlockColumns(820)}칸)` }, + { key: "mobile_landscape", label: `모바일 가로 (${getBlockColumns(600)}칸)` }, + { key: "mobile_portrait", label: `모바일 세로 (${getBlockColumns(375)}칸)` }, ]; const handleVisibilityChange = (mode: GridMode, visible: boolean) => { diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index a9c7db6e..373bed9b 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -13,6 +13,10 @@ import { GridBreakpoint, detectGridMode, PopComponentType, + BLOCK_SIZE, + BLOCK_GAP, + BLOCK_PADDING, + getBlockColumns, } from "../types/pop-layout"; import { convertAndResolvePositions, @@ -107,18 +111,27 @@ export default function PopRenderer({ }: PopRendererProps) { const { gridConfig, components, overrides } = layout; - // 현재 모드 (자동 감지 또는 지정) + // V6: 뷰포트 너비에서 블록 칸 수 동적 계산 const mode = currentMode || detectGridMode(viewportWidth); - const breakpoint = GRID_BREAKPOINTS[mode]; + const columns = getBlockColumns(viewportWidth); - // Gap/Padding: 오버라이드 우선, 없으면 기본값 사용 - const finalGap = overrideGap !== undefined ? overrideGap : breakpoint.gap; - const finalPadding = overridePadding !== undefined ? overridePadding : breakpoint.padding; + // V6: 블록 간격 고정 + const finalGap = overrideGap !== undefined ? overrideGap : BLOCK_GAP; + const finalPadding = overridePadding !== undefined ? overridePadding : BLOCK_PADDING; + + // 하위 호환: breakpoint 객체 (ResizeHandles 등에서 사용) + const breakpoint: GridBreakpoint = { + columns, + rowHeight: BLOCK_SIZE, + gap: finalGap, + padding: finalPadding, + label: `${columns}칸 블록`, + }; // 숨김 컴포넌트 ID 목록 const hiddenIds = overrides?.[mode]?.hidden || []; - // 동적 행 수 계산 (가이드 셀 + Grid 스타일 공유, 숨김 컴포넌트 제외) + // 동적 행 수 계산 const dynamicRowCount = useMemo(() => { const visibleComps = Object.values(components).filter( comp => !hiddenIds.includes(comp.id) @@ -131,19 +144,17 @@ export default function PopRenderer({ return Math.max(10, maxRowEnd + 3); }, [components, overrides, mode, hiddenIds]); - // CSS Grid 스타일 - // 디자인 모드: 행 높이 고정 (정밀한 레이아웃 편집) - // 뷰어 모드: minmax(rowHeight, auto) (컴포넌트가 컨텐츠에 맞게 확장 가능) + // V6: CSS Grid - 열은 1fr(뷰포트 꽉 채움), 행은 고정 BLOCK_SIZE const rowTemplate = isDesignMode - ? `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)` - : `repeat(${dynamicRowCount}, minmax(${breakpoint.rowHeight}px, auto))`; + ? `repeat(${dynamicRowCount}, ${BLOCK_SIZE}px)` + : `repeat(${dynamicRowCount}, minmax(${BLOCK_SIZE}px, auto))`; const autoRowHeight = isDesignMode - ? `${breakpoint.rowHeight}px` - : `minmax(${breakpoint.rowHeight}px, auto)`; + ? `${BLOCK_SIZE}px` + : `minmax(${BLOCK_SIZE}px, auto)`; const gridStyle = useMemo((): React.CSSProperties => ({ display: "grid", - gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`, + gridTemplateColumns: `repeat(${columns}, 1fr)`, gridTemplateRows: rowTemplate, gridAutoRows: autoRowHeight, gap: `${finalGap}px`, @@ -151,15 +162,15 @@ export default function PopRenderer({ minHeight: "100%", backgroundColor: "#ffffff", position: "relative", - }), [breakpoint, finalGap, finalPadding, dynamicRowCount, rowTemplate, autoRowHeight]); + }), [columns, finalGap, finalPadding, dynamicRowCount, rowTemplate, autoRowHeight]); - // 그리드 가이드 셀 생성 (동적 행 수) + // 그리드 가이드 셀 생성 const gridCells = useMemo(() => { if (!isDesignMode || !showGridGuide) return []; const cells = []; for (let row = 1; row <= dynamicRowCount; row++) { - for (let col = 1; col <= breakpoint.columns; col++) { + for (let col = 1; col <= columns; col++) { cells.push({ id: `cell-${col}-${row}`, col, @@ -168,7 +179,7 @@ export default function PopRenderer({ } } return cells; - }, [isDesignMode, showGridGuide, breakpoint.columns, dynamicRowCount]); + }, [isDesignMode, showGridGuide, columns, dynamicRowCount]); // visibility 체크 const isVisible = (comp: PopComponentDefinitionV5): boolean => { diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 9fb9a847..44e4c1c4 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -99,24 +99,39 @@ export interface PopLayoutMetadata { } // ======================================== -// v5 그리드 기반 레이아웃 +// v6 정사각형 블록 그리드 시스템 // ======================================== -// 핵심: CSS Grid로 정확한 위치 지정 -// - 열/행 좌표로 배치 (col, row) -// - 칸 단위 크기 (colSpan, rowSpan) -// - Material Design 브레이크포인트 기반 +// 핵심: 균일한 정사각형 블록 (24px x 24px) +// - 열/행 좌표로 배치 (col, row) - 블록 단위 +// - 뷰포트 너비에 따라 칸 수 동적 계산 +// - 단일 좌표계 (모드별 변환 불필요) /** - * 그리드 모드 (4가지) + * V6 블록 상수 + */ +export const BLOCK_SIZE = 24; // 블록 크기 (px, 정사각형) +export const BLOCK_GAP = 2; // 블록 간격 (px) +export const BLOCK_PADDING = 8; // 캔버스 패딩 (px) + +/** + * 뷰포트 너비에서 블록 칸 수 계산 + */ +export function getBlockColumns(viewportWidth: number): number { + const available = viewportWidth - BLOCK_PADDING * 2; + return Math.max(1, Math.floor((available + BLOCK_GAP) / (BLOCK_SIZE + BLOCK_GAP))); +} + +/** + * 그리드 모드 (하위 호환용 - V6에서는 뷰포트 프리셋 라벨로만 사용) */ export type GridMode = - | "mobile_portrait" // 4칸 - | "mobile_landscape" // 6칸 - | "tablet_portrait" // 8칸 - | "tablet_landscape"; // 12칸 (기본) + | "mobile_portrait" + | "mobile_landscape" + | "tablet_portrait" + | "tablet_landscape"; /** - * 그리드 브레이크포인트 설정 + * 그리드 브레이크포인트 설정 (하위 호환용) */ export interface GridBreakpoint { minWidth?: number; @@ -129,50 +144,43 @@ export interface GridBreakpoint { } /** - * 브레이크포인트 상수 - * 업계 표준 (768px, 1024px) + 실제 기기 커버리지 기반 + * V6 브레이크포인트 (블록 기반 동적 칸 수) + * columns는 각 뷰포트 너비에서의 블록 수 */ export const GRID_BREAKPOINTS: Record = { - // 스마트폰 세로 (iPhone SE ~ Galaxy S25 Ultra) mobile_portrait: { maxWidth: 479, - columns: 4, - rowHeight: 40, - gap: 8, - padding: 12, - label: "모바일 세로 (4칸)", + columns: getBlockColumns(375), + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + label: `모바일 세로 (${getBlockColumns(375)}칸)`, }, - - // 스마트폰 가로 + 소형 태블릿 mobile_landscape: { minWidth: 480, maxWidth: 767, - columns: 6, - rowHeight: 44, - gap: 8, - padding: 16, - label: "모바일 가로 (6칸)", + columns: getBlockColumns(600), + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + label: `모바일 가로 (${getBlockColumns(600)}칸)`, }, - - // 태블릿 세로 (iPad Mini ~ iPad Pro) tablet_portrait: { minWidth: 768, maxWidth: 1023, - columns: 8, - rowHeight: 48, - gap: 12, - padding: 16, - label: "태블릿 세로 (8칸)", + columns: getBlockColumns(820), + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + label: `태블릿 세로 (${getBlockColumns(820)}칸)`, }, - - // 태블릿 가로 + 데스크톱 (기본) tablet_landscape: { minWidth: 1024, - columns: 12, - rowHeight: 48, - gap: 16, - padding: 24, - label: "태블릿 가로 (12칸)", + columns: getBlockColumns(1024), + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + label: `태블릿 가로 (${getBlockColumns(1024)}칸)`, }, } as const; @@ -183,7 +191,6 @@ export const DEFAULT_GRID_MODE: GridMode = "tablet_landscape"; /** * 뷰포트 너비로 모드 감지 - * GRID_BREAKPOINTS와 일치하는 브레이크포인트 사용 */ export function detectGridMode(viewportWidth: number): GridMode { if (viewportWidth < 480) return "mobile_portrait"; @@ -225,17 +232,17 @@ export interface PopLayoutDataV5 { } /** - * 그리드 설정 + * 그리드 설정 (V6: 블록 단위) */ export interface PopGridConfig { - // 행 높이 (px) - 1행의 기본 높이 - rowHeight: number; // 기본 48px + // 행 높이 = 블록 크기 (px) + rowHeight: number; // V6 기본 24px (= BLOCK_SIZE) // 간격 (px) - gap: number; // 기본 8px + gap: number; // V6 기본 2px (= BLOCK_GAP) // 패딩 (px) - padding: number; // 기본 16px + padding: number; // V6 기본 8px (= BLOCK_PADDING) } /** @@ -274,7 +281,7 @@ export interface PopComponentDefinitionV5 { } /** - * Gap 프리셋 타입 + * Gap 프리셋 타입 (V6: 단일 간격이므로 medium만 유효, 하위 호환용 유지) */ export type GapPreset = "narrow" | "medium" | "wide"; @@ -287,12 +294,12 @@ export interface GapPresetConfig { } /** - * Gap 프리셋 상수 + * Gap 프리셋 상수 (V6: 모두 동일 - 블록 간격 고정) */ export const GAP_PRESETS: Record = { - narrow: { multiplier: 0.5, label: "좁게" }, - medium: { multiplier: 1.0, label: "보통" }, - wide: { multiplier: 1.5, label: "넓게" }, + narrow: { multiplier: 1.0, label: "기본" }, + medium: { multiplier: 1.0, label: "기본" }, + wide: { multiplier: 1.0, label: "기본" }, }; /** @@ -330,9 +337,9 @@ export interface PopModeOverrideV5 { export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ version: "pop-5.0", gridConfig: { - rowHeight: 48, - gap: 8, - padding: 16, + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, }, components: {}, dataFlow: { connections: [] }, @@ -351,22 +358,27 @@ export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => { }; /** - * 컴포넌트 타입별 기본 크기 (칸 단위) + * 컴포넌트 타입별 기본 크기 (블록 단위, V6) + * + * 소형 (2x2) : 최소 단위. 아이콘, 프로필, 스캐너 등 단일 요소 + * 중형 (8x4) : 검색, 버튼, 텍스트 등 한 줄 입력/표시 + * 대형 (8x6) : 샘플, 상태바, 필드 등 여러 줄 컨텐츠 + * 초대형 (19x8~) : 카드, 리스트, 대시보드 등 메인 영역 */ export const DEFAULT_COMPONENT_GRID_SIZE: Record = { - "pop-sample": { colSpan: 2, rowSpan: 1 }, - "pop-text": { colSpan: 3, rowSpan: 1 }, - "pop-icon": { colSpan: 1, rowSpan: 2 }, - "pop-dashboard": { colSpan: 6, rowSpan: 3 }, - "pop-card-list": { colSpan: 4, rowSpan: 3 }, - "pop-card-list-v2": { colSpan: 4, rowSpan: 3 }, - "pop-button": { colSpan: 2, rowSpan: 1 }, - "pop-string-list": { colSpan: 4, rowSpan: 3 }, - "pop-search": { colSpan: 2, rowSpan: 1 }, - "pop-status-bar": { colSpan: 6, rowSpan: 1 }, - "pop-field": { colSpan: 6, rowSpan: 2 }, - "pop-scanner": { colSpan: 1, rowSpan: 1 }, - "pop-profile": { colSpan: 1, rowSpan: 1 }, + "pop-sample": { colSpan: 8, rowSpan: 6 }, + "pop-text": { colSpan: 8, rowSpan: 4 }, + "pop-icon": { colSpan: 2, rowSpan: 2 }, + "pop-dashboard": { colSpan: 19, rowSpan: 10 }, + "pop-card-list": { colSpan: 19, rowSpan: 10 }, + "pop-card-list-v2": { colSpan: 19, rowSpan: 10 }, + "pop-button": { colSpan: 8, rowSpan: 4 }, + "pop-string-list": { colSpan: 19, rowSpan: 10 }, + "pop-search": { colSpan: 8, rowSpan: 4 }, + "pop-status-bar": { colSpan: 19, rowSpan: 4 }, + "pop-field": { colSpan: 19, rowSpan: 6 }, + "pop-scanner": { colSpan: 2, rowSpan: 2 }, + "pop-profile": { colSpan: 2, rowSpan: 2 }, }; /** diff --git a/frontend/components/pop/designer/utils/gridUtils.ts b/frontend/components/pop/designer/utils/gridUtils.ts index 308ce730..e5078f64 100644 --- a/frontend/components/pop/designer/utils/gridUtils.ts +++ b/frontend/components/pop/designer/utils/gridUtils.ts @@ -6,196 +6,148 @@ import { GapPreset, GAP_PRESETS, PopLayoutDataV5, - PopComponentDefinitionV5, + PopComponentDefinitionV5, + BLOCK_SIZE, + BLOCK_GAP, + BLOCK_PADDING, + getBlockColumns, } from "../types/pop-layout"; // ======================================== -// Gap/Padding 조정 +// Gap/Padding 조정 (V6: 블록 간격 고정이므로 항상 원본 반환) // ======================================== -/** - * Gap 프리셋에 따라 breakpoint의 gap/padding 조정 - * - * @param base 기본 breakpoint 설정 - * @param preset Gap 프리셋 ("narrow" | "medium" | "wide") - * @returns 조정된 breakpoint (gap, padding 계산됨) - */ export function getAdjustedBreakpoint( base: GridBreakpoint, preset: GapPreset ): GridBreakpoint { - const multiplier = GAP_PRESETS[preset]?.multiplier || 1.0; - - return { - ...base, - gap: Math.round(base.gap * multiplier), - padding: Math.max(8, Math.round(base.padding * multiplier)), // 최소 8px - }; + return { ...base }; } // ======================================== -// 그리드 위치 변환 +// 그리드 위치 변환 (V6: 단일 좌표계이므로 변환 불필요) // ======================================== /** - * 12칸 기준 위치를 다른 모드로 변환 + * V6: 단일 좌표계이므로 변환 없이 원본 반환 + * @deprecated V6에서는 좌표 변환이 불필요합니다 */ export function convertPositionToMode( position: PopGridPosition, targetMode: GridMode ): PopGridPosition { - const sourceColumns = 12; - const targetColumns = GRID_BREAKPOINTS[targetMode].columns; - - // 같은 칸 수면 그대로 반환 - if (sourceColumns === targetColumns) { - return position; - } - - const ratio = targetColumns / sourceColumns; - - // 열 위치 변환 - let newCol = Math.max(1, Math.ceil((position.col - 1) * ratio) + 1); - let newColSpan = Math.max(1, Math.round(position.colSpan * ratio)); - - // 범위 초과 방지 - if (newCol > targetColumns) { - newCol = 1; - } - if (newCol + newColSpan - 1 > targetColumns) { - newColSpan = targetColumns - newCol + 1; - } - - return { - col: newCol, - row: position.row, - colSpan: Math.max(1, newColSpan), - rowSpan: position.rowSpan, - }; + return position; } /** - * 여러 컴포넌트를 모드별로 변환하고 겹침 해결 - * - * v5.1 자동 줄바꿈: - * - 원본 col > targetColumns인 컴포넌트는 자동으로 맨 아래에 배치 - * - 정보 손실 방지: 모든 컴포넌트가 그리드 안에 배치됨 + * V6 행 그룹 리플로우 (방식 F) + * + * 원리: CSS Flexbox wrap과 동일. + * 1. 같은 행의 컴포넌트를 한 묶음으로 처리 + * 2. 최소 2x2칸 보장 (터치 가능한 최소 크기) + * 3. 한 줄에 안 들어가면 다음 줄로 줄바꿈 (숨김 없음) + * 4. 설계 너비의 50% 이상 → 전체 너비 확장 + * 5. 리플로우 후 겹침 해결 (resolveOverlaps) */ export function convertAndResolvePositions( components: Array<{ id: string; position: PopGridPosition }>, targetMode: GridMode ): Array<{ id: string; position: PopGridPosition }> { - // 엣지 케이스: 빈 배열 - if (components.length === 0) { - return []; - } + if (components.length === 0) return []; const targetColumns = GRID_BREAKPOINTS[targetMode].columns; + const designColumns = GRID_BREAKPOINTS["tablet_landscape"].columns; - // 1단계: 각 컴포넌트를 비율로 변환 (원본 col 보존) - const converted = components.map(comp => ({ - id: comp.id, - position: convertPositionToMode(comp.position, targetMode), - originalCol: comp.position.col, // 원본 col 보존 - })); + if (targetColumns >= designColumns) { + return components.map(c => ({ id: c.id, position: { ...c.position } })); + } - // 2단계: 정상 컴포넌트 vs 초과 컴포넌트 분리 - const normalComponents = converted.filter(c => c.originalCol <= targetColumns); - const overflowComponents = converted.filter(c => c.originalCol > targetColumns); + const ratio = targetColumns / designColumns; + const MIN_COL_SPAN = 2; + const MIN_ROW_SPAN = 2; - // 3단계: 정상 컴포넌트의 최대 row 계산 - const maxRow = normalComponents.length > 0 - ? Math.max(...normalComponents.map(c => c.position.row + c.position.rowSpan - 1)) - : 0; - - // 4단계: 초과 컴포넌트들을 맨 아래에 순차 배치 - let currentRow = maxRow + 1; - const wrappedComponents = overflowComponents.map(comp => { - const wrappedPosition: PopGridPosition = { - col: 1, // 왼쪽 끝부터 시작 - row: currentRow, - colSpan: Math.min(comp.position.colSpan, targetColumns), // 최대 칸 수 제한 - rowSpan: comp.position.rowSpan, - }; - currentRow += comp.position.rowSpan; // 다음 행으로 이동 - - return { - id: comp.id, - position: wrappedPosition, - }; + // 1. 원본 row 기준 그룹핑 + const rowGroups: Record> = {}; + components.forEach(comp => { + const r = comp.position.row; + if (!rowGroups[r]) rowGroups[r] = []; + rowGroups[r].push(comp); }); - // 5단계: 정상 + 줄바꿈 컴포넌트 병합 - const adjusted = [ - ...normalComponents.map(c => ({ id: c.id, position: c.position })), - ...wrappedComponents, - ]; + const placed: Array<{ id: string; position: PopGridPosition }> = []; + let outputRow = 1; - // 6단계: 겹침 해결 (아래로 밀기) - return resolveOverlaps(adjusted, targetColumns); + // 2. 각 행 그룹을 순서대로 처리 + const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); + + for (const rowKey of sortedRows) { + const group = rowGroups[rowKey].sort((a, b) => a.position.col - b.position.col); + let currentCol = 1; + let maxRowSpanInLine = 0; + + for (const comp of group) { + const pos = comp.position; + const isMainContent = pos.colSpan >= designColumns * 0.5; + + let scaledSpan = isMainContent + ? targetColumns + : Math.max(MIN_COL_SPAN, Math.round(pos.colSpan * ratio)); + scaledSpan = Math.min(scaledSpan, targetColumns); + + const scaledRowSpan = Math.max(MIN_ROW_SPAN, pos.rowSpan); + + // 현재 줄에 안 들어가면 줄바꿈 + if (currentCol + scaledSpan - 1 > targetColumns) { + outputRow += Math.max(1, maxRowSpanInLine); + currentCol = 1; + maxRowSpanInLine = 0; + } + + placed.push({ + id: comp.id, + position: { + col: currentCol, + row: outputRow, + colSpan: scaledSpan, + rowSpan: scaledRowSpan, + }, + }); + + maxRowSpanInLine = Math.max(maxRowSpanInLine, scaledRowSpan); + currentCol += scaledSpan; + } + + outputRow += Math.max(1, maxRowSpanInLine); + } + + // 3. 겹침 해결 (행 그룹 간 rowSpan 충돌 처리) + return resolveOverlaps(placed, targetColumns); } // ======================================== -// 검토 필요 판별 +// 검토 필요 판별 (V6: 자동 줄바꿈이므로 검토 필요 없음) // ======================================== /** - * 컴포넌트가 현재 모드에서 "검토 필요" 상태인지 확인 - * - * v5.1 검토 필요 기준: - * - 12칸 모드(기본 모드)가 아님 - * - 해당 모드에서 오버라이드가 없음 (아직 편집 안 함) - * - * @param currentMode 현재 그리드 모드 - * @param hasOverride 해당 모드에서 오버라이드 존재 여부 - * @returns true = 검토 필요, false = 검토 완료 또는 불필요 + * V6: 단일 좌표계 + 자동 줄바꿈이므로 검토 필요 없음 + * 항상 false 반환 */ export function needsReview( currentMode: GridMode, hasOverride: boolean ): boolean { - const targetColumns = GRID_BREAKPOINTS[currentMode].columns; - - // 12칸 모드는 기본 모드이므로 검토 불필요 - if (targetColumns === 12) { - return false; - } - - // 오버라이드가 있으면 이미 편집함 → 검토 완료 - if (hasOverride) { - return false; - } - - // 오버라이드 없으면 → 검토 필요 - return true; + return false; } /** - * @deprecated v5.1부터 needsReview() 사용 권장 - * - * 기존 isOutOfBounds는 "화면 밖" 개념이었으나, - * v5.1 자동 줄바꿈으로 인해 모든 컴포넌트가 그리드 안에 배치됩니다. - * 대신 needsReview()로 "검토 필요" 여부를 판별하세요. + * @deprecated V6에서는 자동 줄바꿈이므로 화면 밖 개념 없음 */ export function isOutOfBounds( originalPosition: PopGridPosition, currentMode: GridMode, overridePosition?: PopGridPosition | null ): boolean { - const targetColumns = GRID_BREAKPOINTS[currentMode].columns; - - // 12칸 모드면 초과 불가 - if (targetColumns === 12) { - return false; - } - - // 오버라이드가 있으면 오버라이드 위치로 판단 - if (overridePosition) { - return overridePosition.col > targetColumns; - } - - // 오버라이드 없으면 원본 col로 판단 - return originalPosition.col > targetColumns; + return false; } // ======================================== @@ -269,12 +221,8 @@ export function resolveOverlaps( // ======================================== /** - * 마우스 좌표 → 그리드 좌표 변환 - * - * CSS Grid 계산 방식: - * - 사용 가능 너비 = 캔버스 너비 - 패딩*2 - gap*(columns-1) - * - 각 칸 너비 = 사용 가능 너비 / columns - * - 셀 N의 시작 X = padding + (N-1) * (칸너비 + gap) + * V6: 마우스 좌표 → 블록 그리드 좌표 변환 + * 블록 크기가 고정(BLOCK_SIZE)이므로 계산이 단순함 */ export function mouseToGridPosition( mouseX: number, @@ -285,28 +233,19 @@ export function mouseToGridPosition( gap: number, padding: number ): { col: number; row: number } { - // 캔버스 내 상대 위치 (패딩 영역 포함) const relX = mouseX - canvasRect.left - padding; const relY = mouseY - canvasRect.top - padding; - // CSS Grid 1fr 계산과 동일하게 - // 사용 가능 너비 = 전체 너비 - 양쪽 패딩 - (칸 사이 gap) - const availableWidth = canvasRect.width - padding * 2 - gap * (columns - 1); - const colWidth = availableWidth / columns; + const cellStride = BLOCK_SIZE + gap; - // 각 셀의 실제 간격 (셀 너비 + gap) - const cellStride = colWidth + gap; - - // 그리드 좌표 계산 (1부터 시작) - // relX를 cellStride로 나누면 몇 번째 칸인지 알 수 있음 const col = Math.max(1, Math.min(columns, Math.floor(relX / cellStride) + 1)); - const row = Math.max(1, Math.floor(relY / (rowHeight + gap)) + 1); + const row = Math.max(1, Math.floor(relY / cellStride) + 1); return { col, row }; } /** - * 그리드 좌표 → 픽셀 좌표 변환 + * V6: 블록 그리드 좌표 → 픽셀 좌표 변환 */ export function gridToPixelPosition( col: number, @@ -319,14 +258,13 @@ export function gridToPixelPosition( gap: number, padding: number ): { x: number; y: number; width: number; height: number } { - const totalGap = gap * (columns - 1); - const colWidth = (canvasWidth - padding * 2 - totalGap) / columns; + const cellStride = BLOCK_SIZE + gap; return { - x: padding + (col - 1) * (colWidth + gap), - y: padding + (row - 1) * (rowHeight + gap), - width: colWidth * colSpan + gap * (colSpan - 1), - height: rowHeight * rowSpan + gap * (rowSpan - 1), + x: padding + (col - 1) * cellStride, + y: padding + (row - 1) * cellStride, + width: BLOCK_SIZE * colSpan + gap * (colSpan - 1), + height: BLOCK_SIZE * rowSpan + gap * (rowSpan - 1), }; } @@ -560,3 +498,126 @@ export function getAllEffectivePositions( return result; } + +// ======================================== +// V5 → V6 런타임 변환 (DB 미수정, 로드 시 변환) +// ======================================== + +const V5_BASE_COLUMNS = 12; +const V5_BASE_ROW_HEIGHT = 48; +const V5_BASE_GAP = 16; +const V5_DESIGN_WIDTH = 1024; + +/** + * V5 레이아웃 판별: gridConfig.rowHeight가 V5 기본값(48)이고 + * 좌표가 12칸 체계인 경우만 V5로 판정 + */ +function isV5GridConfig(layout: PopLayoutDataV5): boolean { + if (layout.gridConfig?.rowHeight === BLOCK_SIZE) return false; + + const maxCol = Object.values(layout.components).reduce((max, comp) => { + const end = comp.position.col + comp.position.colSpan - 1; + return Math.max(max, end); + }, 0); + + return maxCol <= V5_BASE_COLUMNS; +} + +function convertV5PositionToV6( + pos: PopGridPosition, + v6DesignColumns: number, +): PopGridPosition { + const colRatio = v6DesignColumns / V5_BASE_COLUMNS; + const rowRatio = (V5_BASE_ROW_HEIGHT + V5_BASE_GAP) / (BLOCK_SIZE + BLOCK_GAP); + + const newCol = Math.max(1, Math.round((pos.col - 1) * colRatio) + 1); + let newColSpan = Math.max(1, Math.round(pos.colSpan * colRatio)); + const newRowSpan = Math.max(1, Math.round(pos.rowSpan * rowRatio)); + + if (newCol + newColSpan - 1 > v6DesignColumns) { + newColSpan = v6DesignColumns - newCol + 1; + } + + return { col: newCol, row: pos.row, colSpan: newColSpan, rowSpan: newRowSpan }; +} + +/** + * V5 레이아웃을 V6 블록 좌표로 런타임 변환 + * - 기본 모드(tablet_landscape) 좌표를 블록 단위로 변환 + * - 모드별 overrides 폐기 (자동 줄바꿈으로 대체) + * - DB 데이터는 건드리지 않음 (메모리에서만 변환) + */ +export function convertV5LayoutToV6(layout: PopLayoutDataV5): PopLayoutDataV5 { + // V5 오버라이드는 V6에서 무효 (4/6/8칸용 좌표가 13/22/31칸에 맞지 않음) + // 좌표 변환 필요 여부와 무관하게 항상 제거 + if (!isV5GridConfig(layout)) { + return { + ...layout, + gridConfig: { + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, + }, + overrides: undefined, + }; + } + + const v6Columns = getBlockColumns(V5_DESIGN_WIDTH); + + const rowGroups: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + const r = comp.position.row; + if (!rowGroups[r]) rowGroups[r] = []; + rowGroups[r].push(id); + }); + + const convertedPositions: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + convertedPositions[id] = convertV5PositionToV6(comp.position, v6Columns); + }); + + const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); + const rowMapping: Record = {}; + let v6Row = 1; + for (const v5Row of sortedRows) { + rowMapping[v5Row] = v6Row; + const maxSpan = Math.max( + ...rowGroups[v5Row].map(id => convertedPositions[id].rowSpan) + ); + v6Row += maxSpan; + } + + const newComponents = { ...layout.components }; + Object.entries(newComponents).forEach(([id, comp]) => { + const converted = convertedPositions[id]; + const mappedRow = rowMapping[comp.position.row] ?? converted.row; + newComponents[id] = { + ...comp, + position: { ...converted, row: mappedRow }, + }; + }); + + const newModals = layout.modals?.map(modal => { + const modalComps = { ...modal.components }; + Object.entries(modalComps).forEach(([id, comp]) => { + modalComps[id] = { + ...comp, + position: convertV5PositionToV6(comp.position, v6Columns), + }; + }); + return { + ...modal, + gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, + components: modalComps, + overrides: undefined, + }; + }); + + return { + ...layout, + gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, + components: newComponents, + overrides: undefined, + modals: newModals, + }; +} diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx index 1c351cf2..f5d06036 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx @@ -295,8 +295,8 @@ function BasicSettingsTab({ const recommendation = useMemo(() => { if (!currentMode) return null; const cols = GRID_BREAKPOINTS[currentMode].columns; - if (cols >= 8) return { rows: 3, cols: 2 }; - if (cols >= 6) return { rows: 3, cols: 1 }; + if (cols >= 25) return { rows: 3, cols: 2 }; + if (cols >= 18) return { rows: 3, cols: 1 }; return { rows: 2, cols: 1 }; }, [currentMode]); From 320100c4e2581e79dd69c622e2c5196e8560e713 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 13 Mar 2026 16:32:20 +0900 Subject: [PATCH 08/18] =?UTF-8?q?refactor:=20POP=20=EA=B7=B8=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=AA=85=EC=B9=AD=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20+=20Dead=20Code=20=EC=A0=9C=EA=B1=B0=20V5?= =?UTF-8?q?=E2=86=92V6=20=EC=A0=84=ED=99=98=20=EA=B3=BC=EC=A0=95=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=88=84=EC=A0=81=EB=90=9C=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=91=EB=AF=B8=EC=82=AC,=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=ED=95=A8=EC=88=98,=20=EB=A0=88=EA=B1=B0=EC=8B=9C=20=EC=9E=94?= =?UTF-8?q?=EC=9E=AC=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B4=80=EB=A6=AC=EC=84=B1=EC=9D=84=20?= =?UTF-8?q?=ED=99=95=EB=B3=B4=ED=95=9C=EB=8B=A4.=2014=EA=B0=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95,=20365=EC=A4=84=20=EC=88=9C?= =?UTF-8?q?=EA=B0=90.=20[=ED=83=80=EC=9E=85=20=EB=A6=AC=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D]=20(14=EA=B0=9C=20=ED=8C=8C=EC=9D=BC)=20-=20PopLayout?= =?UTF-8?q?DataV5=20=E2=86=92=20PopLayoutData=20-=20PopComponentDefinition?= =?UTF-8?q?V5=20=E2=86=92=20PopComponentDefinition=20-=20PopGlobalSettings?= =?UTF-8?q?V5=20=E2=86=92=20PopGlobalSettings=20-=20PopModeOverrideV5=20?= =?UTF-8?q?=E2=86=92=20PopModeOverride=20-=20createEmptyPopLayoutV5=20?= =?UTF-8?q?=E2=86=92=20createEmptyLayout=20-=20isV5Layout=20=E2=86=92=20is?= =?UTF-8?q?PopLayout=20-=20addComponentToV5Layout=20=E2=86=92=20addCompone?= =?UTF-8?q?ntToLayout=20-=20createComponentDefinitionV5=20=E2=86=92=20crea?= =?UTF-8?q?teComponentDefinition=20-=20=EA=B5=AC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=80=20deprecated=20=EB=B3=84=EC=B9=AD=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=9C=A0=EC=A7=80=20(=ED=95=98=EC=9C=84=20=ED=98=B8?= =?UTF-8?q?=ED=99=98)=20[Dead=20Code=20=EC=82=AD=EC=A0=9C]=20(gridUtils.ts?= =?UTF-8?q?=20-350=EC=A4=84)=20-=20getAdjustedBreakpoint,=20convertPositio?= =?UTF-8?q?nToMode,=20isOutOfBounds,=20=20=20mouseToGridPosition,=20gridTo?= =?UTF-8?q?PixelPosition,=20isValidPosition,=20=20=20clampPosition,=20auto?= =?UTF-8?q?LayoutComponents=20(=EC=A0=84=EB=B6=80=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=200=EA=B1=B4)=20-=20needsReview=20+=20Review?= =?UTF-8?q?Panel/ReviewItem=20(=ED=95=AD=EC=83=81=20false,=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9)=20-=20getEffectiveComponentPosition=20expor?= =?UTF-8?q?t=20=E2=86=92=20=EB=82=B4=EB=B6=80=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98=20[=EB=A0=88=EA=B1=B0=EC=8B=9C?= =?UTF-8?q?=20=EB=A1=9C=EB=8D=94=20=EB=B6=84=EB=A6=AC]=20(=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20legacyLoader.ts)=20-=20convertV5LayoutToV6=20?= =?UTF-8?q?=E2=86=92=20loadLegacyLayout=20(legacyLoader.ts)=20-=20V5=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EC=83=81=EC=88=98/=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20gridUtils=EC=97=90=EC=84=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?[=EC=A3=BC=EC=84=9D=20=EC=A0=95=EB=A6=AC]=20-=20"v5=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C"=20=E2=86=92=20"POP=20=EB=B8=94=EB=A1=9D=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C"=20-=20"=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=ED=98=B8=ED=99=98=EC=9A=A9"=20=E2=86=92=20"=EB=B7=B0=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=ED=94=84=EB=A6=AC=EC=85=8B"=20/=20"=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=84=A4=EC=A0=95=EC=9A=A9"=20-?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=ED=97=A4=EB=8D=94,=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EB=B6=84,=20=ED=95=A8=EC=88=98=20JSDoc=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?0=EA=B1=B4.=20DB=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4.=20=EB=B0=B1?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(pop)/pop/screens/[screenId]/page.tsx | 20 +- .../components/pop/designer/PopCanvas.tsx | 134 +----- .../components/pop/designer/PopDesigner.tsx | 43 +- frontend/components/pop/designer/index.ts | 7 +- .../designer/panels/ComponentEditorPanel.tsx | 22 +- .../pop/designer/panels/ConnectionEditor.tsx | 20 +- .../pop/designer/renderers/PopRenderer.tsx | 20 +- .../pop/designer/types/pop-layout.ts | 98 ++-- .../pop/designer/utils/gridUtils.ts | 419 +----------------- .../pop/designer/utils/legacyLoader.ts | 128 ++++++ .../pop/viewer/PopViewerWithModals.tsx | 6 +- .../PopCardListV2Component.tsx | 8 +- .../registry/pop-components/pop-scanner.tsx | 6 +- .../pop-search/PopSearchConfig.tsx | 8 +- .../pop-status-bar/PopStatusBarConfig.tsx | 2 +- 15 files changed, 301 insertions(+), 640 deletions(-) create mode 100644 frontend/components/pop/designer/utils/legacyLoader.ts diff --git a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx index bf2878a5..c7933033 100644 --- a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx +++ b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx @@ -17,17 +17,17 @@ import { ScreenContextProvider } from "@/contexts/ScreenContext"; import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; import { - PopLayoutDataV5, + PopLayoutData, GridMode, - isV5Layout, - createEmptyPopLayoutV5, + isPopLayout, + createEmptyLayout, GAP_PRESETS, GRID_BREAKPOINTS, BLOCK_GAP, BLOCK_PADDING, detectGridMode, } from "@/components/pop/designer/types/pop-layout"; -import { convertV5LayoutToV6 } from "@/components/pop/designer/utils/gridUtils"; +import { loadLegacyLayout } from "@/components/pop/designer/utils/legacyLoader"; // POP 컴포넌트 자동 등록 (레지스트리 초기화 - PopRenderer보다 먼저 import) import "@/lib/registry/pop-components"; import PopViewerWithModals from "@/components/pop/viewer/PopViewerWithModals"; @@ -82,7 +82,7 @@ function PopScreenViewPage() { const { user } = useAuth(); const [screen, setScreen] = useState(null); - const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + const [layout, setLayout] = useState(createEmptyLayout()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -119,22 +119,22 @@ function PopScreenViewPage() { try { const popLayout = await screenApi.getLayoutPop(screenId); - if (popLayout && isV5Layout(popLayout)) { - const v6Layout = convertV5LayoutToV6(popLayout); + if (popLayout && isPopLayout(popLayout)) { + const v6Layout = loadLegacyLayout(popLayout); setLayout(v6Layout); const componentCount = Object.keys(popLayout.components).length; console.log(`[POP] v5 레이아웃 로드됨: ${componentCount}개 컴포넌트`); } else if (popLayout) { // 다른 버전 레이아웃은 빈 v5로 처리 console.log("[POP] 레거시 레이아웃 감지, 빈 레이아웃으로 시작합니다:", popLayout.version); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } else { console.log("[POP] 레이아웃 없음"); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } } catch (layoutError) { console.warn("[POP] 레이아웃 로드 실패:", layoutError); - setLayout(createEmptyPopLayoutV5()); + setLayout(createEmptyLayout()); } } catch (error) { console.error("[POP] 화면 로드 실패:", error); diff --git a/frontend/components/pop/designer/PopCanvas.tsx b/frontend/components/pop/designer/PopCanvas.tsx index acb654ae..d12422ec 100644 --- a/frontend/components/pop/designer/PopCanvas.tsx +++ b/frontend/components/pop/designer/PopCanvas.tsx @@ -4,8 +4,8 @@ import { useCallback, useRef, useState, useEffect, useMemo } from "react"; import { useDrop } from "react-dnd"; import { cn } from "@/lib/utils"; import { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopComponentType, PopGridPosition, GridMode, @@ -22,7 +22,7 @@ import { BLOCK_PADDING, getBlockColumns, } from "./types/pop-layout"; -import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, AlertTriangle, EyeOff, Monitor, ChevronDown, ChevronUp } from "lucide-react"; +import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, EyeOff, Monitor, ChevronDown, ChevronUp } from "lucide-react"; import { useDrag } from "react-dnd"; import { Button } from "@/components/ui/button"; import { @@ -34,7 +34,7 @@ import { } from "@/components/ui/select"; import { toast } from "sonner"; import PopRenderer from "./renderers/PopRenderer"; -import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions, needsReview } from "./utils/gridUtils"; +import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions } from "./utils/gridUtils"; import { DND_ITEM_TYPES } from "./constants"; /** @@ -95,13 +95,13 @@ const CANVAS_EXTRA_ROWS = 3; // 여유 행 수 // Props // ======================================== interface PopCanvasProps { - layout: PopLayoutDataV5; + layout: PopLayoutData; selectedComponentId: string | null; currentMode: GridMode; onModeChange: (mode: GridMode) => void; onSelectComponent: (id: string | null) => void; onDropComponent: (type: PopComponentType, position: PopGridPosition) => void; - onUpdateComponent: (componentId: string, updates: Partial) => void; + onUpdateComponent: (componentId: string, updates: Partial) => void; onDeleteComponent: (componentId: string) => void; onMoveComponent?: (componentId: string, newPosition: PopGridPosition) => void; onResizeComponent?: (componentId: string, newPosition: PopGridPosition) => void; @@ -163,7 +163,7 @@ export default function PopCanvas({ }, [layout.modals]); // activeCanvasId에 따라 렌더링할 layout 분기 - const activeLayout = useMemo((): PopLayoutDataV5 => { + const activeLayout = useMemo((): PopLayoutData => { if (activeCanvasId === "main") return layout; const modal = layout.modals?.find(m => m.id === activeCanvasId); if (!modal) return layout; // fallback @@ -401,7 +401,7 @@ export default function PopCanvas({ const effectivePositions = getAllEffectivePositions(activeLayout, currentMode); // 이동 중인 컴포넌트의 현재 유효 위치에서 colSpan/rowSpan 가져오기 - // 검토 필요(ReviewPanel에서 클릭)나 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용 + // 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용 const currentEffectivePos = effectivePositions.get(dragItem.componentId); const componentData = layout.components[dragItem.componentId]; @@ -472,22 +472,8 @@ export default function PopCanvas({ ); }, [activeLayout.components, hiddenComponentIds]); - // 검토 필요 컴포넌트 목록 - const reviewComponents = useMemo(() => { - return visibleComponents.filter(comp => { - const hasOverride = !!activeLayout.overrides?.[currentMode]?.positions?.[comp.id]; - return needsReview(currentMode, hasOverride); - }); - }, [visibleComponents, activeLayout.overrides, currentMode]); - - // 검토 패널 표시 여부 (12칸 모드가 아니고, 검토 필요 컴포넌트가 있을 때) - const showReviewPanel = currentMode !== "tablet_landscape" && reviewComponents.length > 0; - - // 12칸 모드가 아닐 때만 패널 표시 - // 숨김 패널: 숨김 컴포넌트가 있거나, 그리드에 컴포넌트가 있을 때 드롭 영역으로 표시 const hasGridComponents = Object.keys(activeLayout.components).length > 0; const showHiddenPanel = currentMode !== "tablet_landscape" && (hiddenComponents.length > 0 || hasGridComponents); - const showRightPanel = showReviewPanel || showHiddenPanel; return (
@@ -668,7 +654,7 @@ export default function PopCanvas({
{/* 오른쪽 패널 영역 (초과 컴포넌트 + 숨김 컴포넌트) */} - {showRightPanel && ( + {showHiddenPanel && (
- {/* 검토 필요 패널 */} - {showReviewPanel && ( - - )} - {/* 숨김 컴포넌트 패널 */} {showHiddenPanel && ( void; -} - -function ReviewPanel({ - components, - selectedComponentId, - onSelectComponent, -}: ReviewPanelProps) { - return ( -
- {/* 헤더 */} -
- - - 검토 필요 ({components.length}개) - -
- - {/* 컴포넌트 목록 */} -
- {components.map((comp) => ( - onSelectComponent(comp.id)} - /> - ))} -
- - {/* 안내 문구 */} -
-

- 자동 배치됨. 클릭하여 확인 후 편집 가능 -

-
-
- ); -} - -// ======================================== -// 검토 필요 아이템 (ReviewPanel 내부) -// ======================================== - -interface ReviewItemProps { - component: PopComponentDefinitionV5; - isSelected: boolean; - onSelect: () => void; -} - -function ReviewItem({ - component, - isSelected, - onSelect, -}: ReviewItemProps) { - return ( -
{ - e.stopPropagation(); - onSelect(); - }} - > - - {component.label || component.id} - - - 자동 배치됨 - -
- ); -} - // ======================================== // 숨김 컴포넌트 영역 (오른쪽 패널) // ======================================== interface HiddenPanelProps { - components: PopComponentDefinitionV5[]; + components: PopComponentDefinition[]; selectedComponentId: string | null; onSelectComponent: (id: string | null) => void; onHideComponent?: (componentId: string) => void; @@ -999,7 +889,7 @@ function HiddenPanel({ // ======================================== interface HiddenItemProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; isSelected: boolean; onSelect: () => void; } diff --git a/frontend/components/pop/designer/PopDesigner.tsx b/frontend/components/pop/designer/PopDesigner.tsx index de131032..259ead41 100644 --- a/frontend/components/pop/designer/PopDesigner.tsx +++ b/frontend/components/pop/designer/PopDesigner.tsx @@ -19,21 +19,22 @@ import PopCanvas from "./PopCanvas"; import ComponentEditorPanel from "./panels/ComponentEditorPanel"; import ComponentPalette from "./panels/ComponentPalette"; import { - PopLayoutDataV5, + PopLayoutData, PopComponentType, - PopComponentDefinitionV5, + PopComponentDefinition, PopGridPosition, GridMode, GapPreset, - createEmptyPopLayoutV5, - isV5Layout, - addComponentToV5Layout, - createComponentDefinitionV5, + createEmptyLayout, + isPopLayout, + addComponentToLayout, + createComponentDefinition, GRID_BREAKPOINTS, PopModalDefinition, PopDataConnection, } from "./types/pop-layout"; -import { getAllEffectivePositions, convertV5LayoutToV6 } from "./utils/gridUtils"; +import { getAllEffectivePositions } from "./utils/gridUtils"; +import { loadLegacyLayout } from "./utils/legacyLoader"; import { screenApi } from "@/lib/api/screen"; import { ScreenDefinition } from "@/types/screen"; import { PopDesignerContext } from "./PopDesignerContext"; @@ -59,10 +60,10 @@ export default function PopDesigner({ // ======================================== // 레이아웃 상태 // ======================================== - const [layout, setLayout] = useState(createEmptyPopLayoutV5()); + const [layout, setLayout] = useState(createEmptyLayout()); // 히스토리 - const [history, setHistory] = useState([]); + const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // UI 상태 @@ -84,7 +85,7 @@ export default function PopDesigner({ const [activeCanvasId, setActiveCanvasId] = useState("main"); // 선택된 컴포넌트 (activeCanvasId에 따라 메인 또는 모달에서 조회) - const selectedComponent: PopComponentDefinitionV5 | null = (() => { + const selectedComponent: PopComponentDefinition | null = (() => { if (!selectedComponentId) return null; if (activeCanvasId === "main") { return layout.components[selectedComponentId] || null; @@ -96,7 +97,7 @@ export default function PopDesigner({ // ======================================== // 히스토리 관리 // ======================================== - const saveToHistory = useCallback((newLayout: PopLayoutDataV5) => { + const saveToHistory = useCallback((newLayout: PopLayoutData) => { setHistory((prev) => { const newHistory = prev.slice(0, historyIndex + 1); newHistory.push(JSON.parse(JSON.stringify(newLayout))); @@ -150,11 +151,11 @@ export default function PopDesigner({ try { const loadedLayout = await screenApi.getLayoutPop(selectedScreen.screenId); - if (loadedLayout && isV5Layout(loadedLayout) && loadedLayout.components && Object.keys(loadedLayout.components).length > 0) { + if (loadedLayout && isPopLayout(loadedLayout) && loadedLayout.components && Object.keys(loadedLayout.components).length > 0) { if (!loadedLayout.settings.gapPreset) { loadedLayout.settings.gapPreset = "medium"; } - const v6Layout = convertV5LayoutToV6(loadedLayout); + const v6Layout = loadLegacyLayout(loadedLayout); setLayout(v6Layout); setHistory([v6Layout]); setHistoryIndex(0); @@ -174,7 +175,7 @@ export default function PopDesigner({ console.log(`POP 레이아웃 로드: ${existingIds.length}개 컴포넌트, idCounter: ${maxId + 1}`); } else { // 새 화면 또는 빈 레이아웃 - const emptyLayout = createEmptyPopLayoutV5(); + const emptyLayout = createEmptyLayout(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); @@ -183,7 +184,7 @@ export default function PopDesigner({ } catch (error) { console.error("레이아웃 로드 실패:", error); toast.error("레이아웃을 불러오는데 실패했습니다"); - const emptyLayout = createEmptyPopLayoutV5(); + const emptyLayout = createEmptyLayout(); setLayout(emptyLayout); setHistory([emptyLayout]); setHistoryIndex(0); @@ -224,13 +225,13 @@ export default function PopDesigner({ if (activeCanvasId === "main") { // 메인 캔버스 - const newLayout = addComponentToV5Layout(layout, componentId, type, position, `${type} ${idCounter}`); + const newLayout = addComponentToLayout(layout, componentId, type, position, `${type} ${idCounter}`); setLayout(newLayout); saveToHistory(newLayout); } else { // 모달 캔버스 setLayout(prev => { - const comp = createComponentDefinitionV5(componentId, type, position, `${type} ${idCounter}`); + const comp = createComponentDefinition(componentId, type, position, `${type} ${idCounter}`); const newLayout = { ...prev, modals: (prev.modals || []).map(m => { @@ -249,7 +250,7 @@ export default function PopDesigner({ ); const handleUpdateComponent = useCallback( - (componentId: string, updates: Partial) => { + (componentId: string, updates: Partial) => { // 함수적 업데이트로 stale closure 방지 setLayout((prev) => { if (activeCanvasId === "main") { @@ -302,7 +303,7 @@ export default function PopDesigner({ const newId = `conn_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; const newConnection: PopDataConnection = { ...conn, id: newId }; const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, @@ -321,7 +322,7 @@ export default function PopDesigner({ (connectionId: string, conn: Omit) => { setLayout((prev) => { const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, @@ -342,7 +343,7 @@ export default function PopDesigner({ (connectionId: string) => { setLayout((prev) => { const prevConnections = prev.dataFlow?.connections || []; - const newLayout: PopLayoutDataV5 = { + const newLayout: PopLayoutData = { ...prev, dataFlow: { ...prev.dataFlow, diff --git a/frontend/components/pop/designer/index.ts b/frontend/components/pop/designer/index.ts index 37d86aec..c58ec3db 100644 --- a/frontend/components/pop/designer/index.ts +++ b/frontend/components/pop/designer/index.ts @@ -1,4 +1,4 @@ -// POP 디자이너 컴포넌트 export (v5 그리드 시스템) +// POP 디자이너 컴포넌트 export (블록 그리드 시스템) // 타입 export * from "./types"; @@ -17,11 +17,12 @@ export { default as PopRenderer } from "./renderers/PopRenderer"; // 유틸리티 export * from "./utils/gridUtils"; +export * from "./utils/legacyLoader"; // 핵심 타입 재export (편의) export type { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopComponentType, PopGridPosition, GridMode, diff --git a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx index ec22426d..d79883ad 100644 --- a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx +++ b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx @@ -3,7 +3,7 @@ import React from "react"; import { cn } from "@/lib/utils"; import { - PopComponentDefinitionV5, + PopComponentDefinition, PopGridPosition, GridMode, GRID_BREAKPOINTS, @@ -33,15 +33,15 @@ import ConnectionEditor from "./ConnectionEditor"; interface ComponentEditorPanelProps { /** 선택된 컴포넌트 */ - component: PopComponentDefinitionV5 | null; + component: PopComponentDefinition | null; /** 현재 모드 */ currentMode: GridMode; /** 컴포넌트 업데이트 */ - onUpdateComponent?: (updates: Partial) => void; + onUpdateComponent?: (updates: Partial) => void; /** 추가 className */ className?: string; /** 그리드에 배치된 모든 컴포넌트 */ - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; /** 컴포넌트 선택 콜백 */ onSelectComponent?: (componentId: string) => void; /** 현재 선택된 컴포넌트 ID */ @@ -249,11 +249,11 @@ export default function ComponentEditorPanel({ // ======================================== interface PositionFormProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; currentMode: GridMode; isDefaultMode: boolean; columns: number; - onUpdate?: (updates: Partial) => void; + onUpdate?: (updates: Partial) => void; } function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate }: PositionFormProps) { @@ -402,13 +402,13 @@ function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate // ======================================== interface ComponentSettingsFormProps { - component: PopComponentDefinitionV5; - onUpdate?: (updates: Partial) => void; + component: PopComponentDefinition; + onUpdate?: (updates: Partial) => void; currentMode?: GridMode; previewPageIndex?: number; onPreviewPage?: (pageIndex: number) => void; modals?: PopModalDefinition[]; - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; connections?: PopDataConnection[]; } @@ -466,8 +466,8 @@ function ComponentSettingsForm({ component, onUpdate, currentMode, previewPageIn // ======================================== interface VisibilityFormProps { - component: PopComponentDefinitionV5; - onUpdate?: (updates: Partial) => void; + component: PopComponentDefinition; + onUpdate?: (updates: Partial) => void; } function VisibilityForm({ component, onUpdate }: VisibilityFormProps) { diff --git a/frontend/components/pop/designer/panels/ConnectionEditor.tsx b/frontend/components/pop/designer/panels/ConnectionEditor.tsx index 84b56935..0a64e82a 100644 --- a/frontend/components/pop/designer/panels/ConnectionEditor.tsx +++ b/frontend/components/pop/designer/panels/ConnectionEditor.tsx @@ -13,7 +13,7 @@ import { SelectValue, } from "@/components/ui/select"; import { - PopComponentDefinitionV5, + PopComponentDefinition, PopDataConnection, } from "../types/pop-layout"; import { @@ -26,8 +26,8 @@ import { getTableColumns } from "@/lib/api/tableManagement"; // ======================================== interface ConnectionEditorProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; connections: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; @@ -102,8 +102,8 @@ export default function ConnectionEditor({ // ======================================== interface SendSectionProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; outgoing: PopDataConnection[]; onAddConnection?: (conn: Omit) => void; onUpdateConnection?: (connectionId: string, conn: Omit) => void; @@ -197,15 +197,15 @@ function SendSection({ // ======================================== interface SimpleConnectionFormProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; initial?: PopDataConnection; onSubmit: (data: Omit) => void; onCancel?: () => void; submitLabel: string; } -function extractSubTableName(comp: PopComponentDefinitionV5): string | null { +function extractSubTableName(comp: PopComponentDefinition): string | null { const cfg = comp.config as Record | undefined; if (!cfg) return null; @@ -423,8 +423,8 @@ function SimpleConnectionForm({ // ======================================== interface ReceiveSectionProps { - component: PopComponentDefinitionV5; - allComponents: PopComponentDefinitionV5[]; + component: PopComponentDefinition; + allComponents: PopComponentDefinition[]; incoming: PopDataConnection[]; } diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 373bed9b..89b4a551 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -5,8 +5,8 @@ import { useDrag } from "react-dnd"; import { cn } from "@/lib/utils"; import { DND_ITEM_TYPES } from "../constants"; import { - PopLayoutDataV5, - PopComponentDefinitionV5, + PopLayoutData, + PopComponentDefinition, PopGridPosition, GridMode, GRID_BREAKPOINTS, @@ -31,7 +31,7 @@ import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry"; interface PopRendererProps { /** v5 레이아웃 데이터 */ - layout: PopLayoutDataV5; + layout: PopLayoutData; /** 현재 뷰포트 너비 */ viewportWidth: number; /** 현재 모드 (자동 감지 또는 수동 지정) */ @@ -182,7 +182,7 @@ export default function PopRenderer({ }, [isDesignMode, showGridGuide, columns, dynamicRowCount]); // visibility 체크 - const isVisible = (comp: PopComponentDefinitionV5): boolean => { + const isVisible = (comp: PopComponentDefinition): boolean => { if (!comp.visibility) return true; const modeVisibility = comp.visibility[mode]; return modeVisibility !== false; @@ -207,7 +207,7 @@ export default function PopRenderer({ }; // 오버라이드 적용 또는 자동 재배치 - const getEffectivePosition = (comp: PopComponentDefinitionV5): PopGridPosition => { + const getEffectivePosition = (comp: PopComponentDefinition): PopGridPosition => { // 1순위: 오버라이드가 있으면 사용 const override = overrides?.[mode]?.positions?.[comp.id]; if (override) { @@ -225,7 +225,7 @@ export default function PopRenderer({ }; // 오버라이드 숨김 체크 - const isHiddenByOverride = (comp: PopComponentDefinitionV5): boolean => { + const isHiddenByOverride = (comp: PopComponentDefinition): boolean => { return overrides?.[mode]?.hidden?.includes(comp.id) ?? false; }; @@ -322,7 +322,7 @@ export default function PopRenderer({ // ======================================== interface DraggableComponentProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; position: PopGridPosition; positionStyle: React.CSSProperties; isSelected: boolean; @@ -423,7 +423,7 @@ function DraggableComponent({ // ======================================== interface ResizeHandlesProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; position: PopGridPosition; breakpoint: GridBreakpoint; viewportWidth: number; @@ -544,7 +544,7 @@ function ResizeHandles({ // ======================================== interface ComponentContentProps { - component: PopComponentDefinitionV5; + component: PopComponentDefinition; effectivePosition: PopGridPosition; isDesignMode: boolean; isSelected: boolean; @@ -614,7 +614,7 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect // ======================================== function renderActualComponent( - component: PopComponentDefinitionV5, + component: PopComponentDefinition, effectivePosition?: PopGridPosition, onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void, screenId?: string, diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 44e4c1c4..7b008caf 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -1,6 +1,4 @@ -// POP 디자이너 레이아웃 타입 정의 -// v5.0: CSS Grid 기반 그리드 시스템 -// 2024-02 버전 통합: v1~v4 제거, v5 단일 버전 +// POP 블록 그리드 레이아웃 타입 정의 // ======================================== // 공통 타입 @@ -122,7 +120,7 @@ export function getBlockColumns(viewportWidth: number): number { } /** - * 그리드 모드 (하위 호환용 - V6에서는 뷰포트 프리셋 라벨로만 사용) + * 뷰포트 프리셋 (디자이너 해상도 전환용) */ export type GridMode = | "mobile_portrait" @@ -131,7 +129,7 @@ export type GridMode = | "tablet_landscape"; /** - * 그리드 브레이크포인트 설정 (하위 호환용) + * 뷰포트 프리셋 설정 */ export interface GridBreakpoint { minWidth?: number; @@ -200,31 +198,31 @@ export function detectGridMode(viewportWidth: number): GridMode { } /** - * v5 레이아웃 (그리드 기반) + * POP 레이아웃 데이터 */ -export interface PopLayoutDataV5 { +export interface PopLayoutData { version: "pop-5.0"; // 그리드 설정 gridConfig: PopGridConfig; // 컴포넌트 정의 (ID → 정의) - components: Record; + components: Record; // 데이터 흐름 dataFlow: PopDataFlow; // 전역 설정 - settings: PopGlobalSettingsV5; + settings: PopGlobalSettings; // 메타데이터 metadata?: PopLayoutMetadata; // 모드별 오버라이드 (위치 변경용) overrides?: { - mobile_portrait?: PopModeOverrideV5; - mobile_landscape?: PopModeOverrideV5; - tablet_portrait?: PopModeOverrideV5; + mobile_portrait?: PopModeOverride; + mobile_landscape?: PopModeOverride; + tablet_portrait?: PopModeOverride; }; // 모달 캔버스 목록 (버튼의 "모달 열기" 액션으로 생성) @@ -256,9 +254,9 @@ export interface PopGridPosition { } /** - * v5 컴포넌트 정의 + * POP 컴포넌트 정의 */ -export interface PopComponentDefinitionV5 { +export interface PopComponentDefinition { id: string; type: PopComponentType; label?: string; @@ -303,9 +301,9 @@ export const GAP_PRESETS: Record = { }; /** - * v5 전역 설정 + * POP 전역 설정 */ -export interface PopGlobalSettingsV5 { +export interface PopGlobalSettings { // 터치 최소 크기 (px) touchTargetMin: number; // 기본 48 @@ -317,9 +315,9 @@ export interface PopGlobalSettingsV5 { } /** - * v5 모드별 오버라이드 + * 모드별 오버라이드 (위치/숨김) */ -export interface PopModeOverrideV5 { +export interface PopModeOverride { // 컴포넌트별 위치 오버라이드 positions?: Record>; @@ -328,13 +326,13 @@ export interface PopModeOverrideV5 { } // ======================================== -// v5 유틸리티 함수 +// 레이아웃 유틸리티 함수 // ======================================== /** - * 빈 v5 레이아웃 생성 + * 빈 POP 레이아웃 생성 */ -export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ +export const createEmptyLayout = (): PopLayoutData => ({ version: "pop-5.0", gridConfig: { rowHeight: BLOCK_SIZE, @@ -351,9 +349,9 @@ export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({ }); /** - * v5 레이아웃 여부 확인 + * POP 레이아웃 데이터인지 확인 */ -export const isV5Layout = (layout: any): layout is PopLayoutDataV5 => { +export const isPopLayout = (layout: any): layout is PopLayoutData => { return layout?.version === "pop-5.0"; }; @@ -382,14 +380,14 @@ export const DEFAULT_COMPONENT_GRID_SIZE: Record ({ +): PopComponentDefinition => ({ id, type, label, @@ -397,21 +395,21 @@ export const createComponentDefinitionV5 = ( }); /** - * v5 레이아웃에 컴포넌트 추가 + * POP 레이아웃에 컴포넌트 추가 */ -export const addComponentToV5Layout = ( - layout: PopLayoutDataV5, +export const addComponentToLayout = ( + layout: PopLayoutData, componentId: string, type: PopComponentType, position: PopGridPosition, label?: string -): PopLayoutDataV5 => { +): PopLayoutData => { const newLayout = { ...layout }; // 컴포넌트 정의 추가 newLayout.components = { ...newLayout.components, - [componentId]: createComponentDefinitionV5(componentId, type, position, label), + [componentId]: createComponentDefinition(componentId, type, position, label), }; return newLayout; @@ -486,12 +484,12 @@ export interface PopModalDefinition { /** 모달 내부 그리드 설정 */ gridConfig: PopGridConfig; /** 모달 내부 컴포넌트 */ - components: Record; + components: Record; /** 모드별 오버라이드 */ overrides?: { - mobile_portrait?: PopModeOverrideV5; - mobile_landscape?: PopModeOverrideV5; - tablet_portrait?: PopModeOverrideV5; + mobile_portrait?: PopModeOverride; + mobile_landscape?: PopModeOverride; + tablet_portrait?: PopModeOverride; }; /** 모달 프레임 설정 (닫기 방식) */ frameConfig?: { @@ -507,15 +505,29 @@ export interface PopModalDefinition { } // ======================================== -// 레거시 타입 별칭 (하위 호환 - 추후 제거) +// 레거시 타입 별칭 (이전 코드 호환용) // ======================================== -// 기존 코드에서 import 오류 방지용 -/** @deprecated v5에서는 PopLayoutDataV5 사용 */ -export type PopLayoutData = PopLayoutDataV5; +/** @deprecated PopLayoutData 사용 */ +export type PopLayoutDataV5 = PopLayoutData; -/** @deprecated v5에서는 PopComponentDefinitionV5 사용 */ -export type PopComponentDefinition = PopComponentDefinitionV5; +/** @deprecated PopComponentDefinition 사용 */ +export type PopComponentDefinitionV5 = PopComponentDefinition; -/** @deprecated v5에서는 PopGridPosition 사용 */ -export type GridPosition = PopGridPosition; +/** @deprecated PopGlobalSettings 사용 */ +export type PopGlobalSettingsV5 = PopGlobalSettings; + +/** @deprecated PopModeOverride 사용 */ +export type PopModeOverrideV5 = PopModeOverride; + +/** @deprecated createEmptyLayout 사용 */ +export const createEmptyPopLayoutV5 = createEmptyLayout; + +/** @deprecated isPopLayout 사용 */ +export const isV5Layout = isPopLayout; + +/** @deprecated addComponentToLayout 사용 */ +export const addComponentToV5Layout = addComponentToLayout; + +/** @deprecated createComponentDefinition 사용 */ +export const createComponentDefinitionV5 = createComponentDefinition; diff --git a/frontend/components/pop/designer/utils/gridUtils.ts b/frontend/components/pop/designer/utils/gridUtils.ts index e5078f64..5a8895d8 100644 --- a/frontend/components/pop/designer/utils/gridUtils.ts +++ b/frontend/components/pop/designer/utils/gridUtils.ts @@ -1,53 +1,25 @@ +// POP 그리드 유틸리티 (리플로우, 겹침 해결, 위치 계산) + import { PopGridPosition, GridMode, GRID_BREAKPOINTS, - GridBreakpoint, - GapPreset, - GAP_PRESETS, - PopLayoutDataV5, - PopComponentDefinitionV5, - BLOCK_SIZE, - BLOCK_GAP, - BLOCK_PADDING, - getBlockColumns, + PopLayoutData, } from "../types/pop-layout"; // ======================================== -// Gap/Padding 조정 (V6: 블록 간격 고정이므로 항상 원본 반환) -// ======================================== - -export function getAdjustedBreakpoint( - base: GridBreakpoint, - preset: GapPreset -): GridBreakpoint { - return { ...base }; -} - -// ======================================== -// 그리드 위치 변환 (V6: 단일 좌표계이므로 변환 불필요) +// 리플로우 (행 그룹 기반 자동 재배치) // ======================================== /** - * V6: 단일 좌표계이므로 변환 없이 원본 반환 - * @deprecated V6에서는 좌표 변환이 불필요합니다 - */ -export function convertPositionToMode( - position: PopGridPosition, - targetMode: GridMode -): PopGridPosition { - return position; -} - -/** - * V6 행 그룹 리플로우 (방식 F) + * 행 그룹 리플로우 * - * 원리: CSS Flexbox wrap과 동일. + * CSS Flexbox wrap 원리로 자동 재배치한다. * 1. 같은 행의 컴포넌트를 한 묶음으로 처리 * 2. 최소 2x2칸 보장 (터치 가능한 최소 크기) * 3. 한 줄에 안 들어가면 다음 줄로 줄바꿈 (숨김 없음) - * 4. 설계 너비의 50% 이상 → 전체 너비 확장 - * 5. 리플로우 후 겹침 해결 (resolveOverlaps) + * 4. 설계 너비의 50% 이상인 컴포넌트는 전체 너비 확장 + * 5. 리플로우 후 겹침 해결 */ export function convertAndResolvePositions( components: Array<{ id: string; position: PopGridPosition }>, @@ -66,7 +38,6 @@ export function convertAndResolvePositions( const MIN_COL_SPAN = 2; const MIN_ROW_SPAN = 2; - // 1. 원본 row 기준 그룹핑 const rowGroups: Record> = {}; components.forEach(comp => { const r = comp.position.row; @@ -77,7 +48,6 @@ export function convertAndResolvePositions( const placed: Array<{ id: string; position: PopGridPosition }> = []; let outputRow = 1; - // 2. 각 행 그룹을 순서대로 처리 const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); for (const rowKey of sortedRows) { @@ -96,7 +66,6 @@ export function convertAndResolvePositions( const scaledRowSpan = Math.max(MIN_ROW_SPAN, pos.rowSpan); - // 현재 줄에 안 들어가면 줄바꿈 if (currentCol + scaledSpan - 1 > targetColumns) { outputRow += Math.max(1, maxRowSpanInLine); currentCol = 1; @@ -120,50 +89,18 @@ export function convertAndResolvePositions( outputRow += Math.max(1, maxRowSpanInLine); } - // 3. 겹침 해결 (행 그룹 간 rowSpan 충돌 처리) return resolveOverlaps(placed, targetColumns); } -// ======================================== -// 검토 필요 판별 (V6: 자동 줄바꿈이므로 검토 필요 없음) -// ======================================== - -/** - * V6: 단일 좌표계 + 자동 줄바꿈이므로 검토 필요 없음 - * 항상 false 반환 - */ -export function needsReview( - currentMode: GridMode, - hasOverride: boolean -): boolean { - return false; -} - -/** - * @deprecated V6에서는 자동 줄바꿈이므로 화면 밖 개념 없음 - */ -export function isOutOfBounds( - originalPosition: PopGridPosition, - currentMode: GridMode, - overridePosition?: PopGridPosition | null -): boolean { - return false; -} - // ======================================== // 겹침 감지 및 해결 // ======================================== -/** - * 두 위치가 겹치는지 확인 - */ export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { - // 열 겹침 체크 const aColEnd = a.col + a.colSpan - 1; const bColEnd = b.col + b.colSpan - 1; const colOverlap = !(aColEnd < b.col || bColEnd < a.col); - // 행 겹침 체크 const aRowEnd = a.row + a.rowSpan - 1; const bRowEnd = b.row + b.rowSpan - 1; const rowOverlap = !(aRowEnd < b.row || bRowEnd < a.row); @@ -171,14 +108,10 @@ export function isOverlapping(a: PopGridPosition, b: PopGridPosition): boolean { return colOverlap && rowOverlap; } -/** - * 겹침 해결 (아래로 밀기) - */ export function resolveOverlaps( positions: Array<{ id: string; position: PopGridPosition }>, columns: number ): Array<{ id: string; position: PopGridPosition }> { - // row, col 순으로 정렬 const sorted = [...positions].sort((a, b) => a.position.row - b.position.row || a.position.col - b.position.col ); @@ -188,21 +121,15 @@ export function resolveOverlaps( sorted.forEach((item) => { let { row, col, colSpan, rowSpan } = item.position; - // 열이 범위를 초과하면 조정 if (col + colSpan - 1 > columns) { colSpan = columns - col + 1; } - // 기존 배치와 겹치면 아래로 이동 let attempts = 0; - const maxAttempts = 100; - - while (attempts < maxAttempts) { + while (attempts < 100) { const currentPos: PopGridPosition = { col, row, colSpan, rowSpan }; const hasOverlap = resolved.some(r => isOverlapping(currentPos, r.position)); - if (!hasOverlap) break; - row++; attempts++; } @@ -217,110 +144,9 @@ export function resolveOverlaps( } // ======================================== -// 좌표 변환 +// 자동 배치 (새 컴포넌트 드롭 시) // ======================================== -/** - * V6: 마우스 좌표 → 블록 그리드 좌표 변환 - * 블록 크기가 고정(BLOCK_SIZE)이므로 계산이 단순함 - */ -export function mouseToGridPosition( - mouseX: number, - mouseY: number, - canvasRect: DOMRect, - columns: number, - rowHeight: number, - gap: number, - padding: number -): { col: number; row: number } { - const relX = mouseX - canvasRect.left - padding; - const relY = mouseY - canvasRect.top - padding; - - const cellStride = BLOCK_SIZE + gap; - - const col = Math.max(1, Math.min(columns, Math.floor(relX / cellStride) + 1)); - const row = Math.max(1, Math.floor(relY / cellStride) + 1); - - return { col, row }; -} - -/** - * V6: 블록 그리드 좌표 → 픽셀 좌표 변환 - */ -export function gridToPixelPosition( - col: number, - row: number, - colSpan: number, - rowSpan: number, - canvasWidth: number, - columns: number, - rowHeight: number, - gap: number, - padding: number -): { x: number; y: number; width: number; height: number } { - const cellStride = BLOCK_SIZE + gap; - - return { - x: padding + (col - 1) * cellStride, - y: padding + (row - 1) * cellStride, - width: BLOCK_SIZE * colSpan + gap * (colSpan - 1), - height: BLOCK_SIZE * rowSpan + gap * (rowSpan - 1), - }; -} - -// ======================================== -// 위치 검증 -// ======================================== - -/** - * 위치가 그리드 범위 내에 있는지 확인 - */ -export function isValidPosition( - position: PopGridPosition, - columns: number -): boolean { - return ( - position.col >= 1 && - position.row >= 1 && - position.colSpan >= 1 && - position.rowSpan >= 1 && - position.col + position.colSpan - 1 <= columns - ); -} - -/** - * 위치를 그리드 범위 내로 조정 - */ -export function clampPosition( - position: PopGridPosition, - columns: number -): PopGridPosition { - let { col, row, colSpan, rowSpan } = position; - - // 최소값 보장 - col = Math.max(1, col); - row = Math.max(1, row); - colSpan = Math.max(1, colSpan); - rowSpan = Math.max(1, rowSpan); - - // 열 범위 초과 방지 - if (col + colSpan - 1 > columns) { - if (col > columns) { - col = 1; - } - colSpan = columns - col + 1; - } - - return { col, row, colSpan, rowSpan }; -} - -// ======================================== -// 자동 배치 -// ======================================== - -/** - * 다음 빈 위치 찾기 - */ export function findNextEmptyPosition( existingPositions: PopGridPosition[], colSpan: number, @@ -329,168 +155,94 @@ export function findNextEmptyPosition( ): PopGridPosition { let row = 1; let col = 1; - - const maxAttempts = 1000; let attempts = 0; - while (attempts < maxAttempts) { + while (attempts < 1000) { const candidatePos: PopGridPosition = { col, row, colSpan, rowSpan }; - // 범위 체크 if (col + colSpan - 1 > columns) { col = 1; row++; continue; } - // 겹침 체크 - const hasOverlap = existingPositions.some(pos => - isOverlapping(candidatePos, pos) - ); + const hasOverlap = existingPositions.some(pos => isOverlapping(candidatePos, pos)); + if (!hasOverlap) return candidatePos; - if (!hasOverlap) { - return candidatePos; - } - - // 다음 위치로 이동 col++; if (col + colSpan - 1 > columns) { col = 1; row++; } - attempts++; } - // 실패 시 마지막 행에 배치 return { col: 1, row: row + 1, colSpan, rowSpan }; } -/** - * 컴포넌트들을 자동으로 배치 - */ -export function autoLayoutComponents( - components: Array<{ id: string; colSpan: number; rowSpan: number }>, - columns: number -): Array<{ id: string; position: PopGridPosition }> { - const result: Array<{ id: string; position: PopGridPosition }> = []; - - let currentRow = 1; - let currentCol = 1; - - components.forEach(comp => { - // 현재 행에 공간이 부족하면 다음 행으로 - if (currentCol + comp.colSpan - 1 > columns) { - currentRow++; - currentCol = 1; - } - - result.push({ - id: comp.id, - position: { - col: currentCol, - row: currentRow, - colSpan: comp.colSpan, - rowSpan: comp.rowSpan, - }, - }); - - currentCol += comp.colSpan; - }); - - return result; -} - // ======================================== -// 유효 위치 계산 (통합 함수) +// 유효 위치 계산 // ======================================== /** - * 컴포넌트의 유효 위치를 계산합니다. + * 컴포넌트의 유효 위치를 계산한다. * 우선순위: 1. 오버라이드 → 2. 자동 재배치 → 3. 원본 위치 - * - * @param componentId 컴포넌트 ID - * @param layout 전체 레이아웃 데이터 - * @param mode 현재 그리드 모드 - * @param autoResolvedPositions 미리 계산된 자동 재배치 위치 (선택적) */ -export function getEffectiveComponentPosition( +function getEffectiveComponentPosition( componentId: string, - layout: PopLayoutDataV5, + layout: PopLayoutData, mode: GridMode, autoResolvedPositions?: Array<{ id: string; position: PopGridPosition }> ): PopGridPosition | null { const component = layout.components[componentId]; if (!component) return null; - // 1순위: 오버라이드가 있으면 사용 const override = layout.overrides?.[mode]?.positions?.[componentId]; if (override) { return { ...component.position, ...override }; } - // 2순위: 자동 재배치된 위치 사용 if (autoResolvedPositions) { const autoResolved = autoResolvedPositions.find(p => p.id === componentId); - if (autoResolved) { - return autoResolved.position; - } + if (autoResolved) return autoResolved.position; } else { - // 자동 재배치 직접 계산 const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({ id, position: comp.position, })); const resolved = convertAndResolvePositions(componentsArray, mode); const autoResolved = resolved.find(p => p.id === componentId); - if (autoResolved) { - return autoResolved.position; - } + if (autoResolved) return autoResolved.position; } - // 3순위: 원본 위치 (12칸 모드) return component.position; } /** - * 모든 컴포넌트의 유효 위치를 일괄 계산합니다. - * 숨김 처리된 컴포넌트는 제외됩니다. - * - * v5.1: 자동 줄바꿈 시스템으로 인해 모든 컴포넌트가 그리드 안에 배치되므로 - * "화면 밖" 개념이 제거되었습니다. + * 모든 컴포넌트의 유효 위치를 일괄 계산한다. + * 숨김 처리된 컴포넌트는 제외. */ export function getAllEffectivePositions( - layout: PopLayoutDataV5, + layout: PopLayoutData, mode: GridMode ): Map { const result = new Map(); - // 숨김 처리된 컴포넌트 ID 목록 const hiddenIds = layout.overrides?.[mode]?.hidden || []; - // 자동 재배치 위치 미리 계산 const componentsArray = Object.entries(layout.components).map(([id, comp]) => ({ id, position: comp.position, })); const autoResolvedPositions = convertAndResolvePositions(componentsArray, mode); - // 각 컴포넌트의 유효 위치 계산 Object.keys(layout.components).forEach(componentId => { - // 숨김 처리된 컴포넌트는 제외 - if (hiddenIds.includes(componentId)) { - return; - } + if (hiddenIds.includes(componentId)) return; const position = getEffectiveComponentPosition( - componentId, - layout, - mode, - autoResolvedPositions + componentId, layout, mode, autoResolvedPositions ); - // v5.1: 자동 줄바꿈으로 인해 모든 컴포넌트가 그리드 안에 있음 - // 따라서 추가 필터링 불필요 if (position) { result.set(componentId, position); } @@ -498,126 +250,3 @@ export function getAllEffectivePositions( return result; } - -// ======================================== -// V5 → V6 런타임 변환 (DB 미수정, 로드 시 변환) -// ======================================== - -const V5_BASE_COLUMNS = 12; -const V5_BASE_ROW_HEIGHT = 48; -const V5_BASE_GAP = 16; -const V5_DESIGN_WIDTH = 1024; - -/** - * V5 레이아웃 판별: gridConfig.rowHeight가 V5 기본값(48)이고 - * 좌표가 12칸 체계인 경우만 V5로 판정 - */ -function isV5GridConfig(layout: PopLayoutDataV5): boolean { - if (layout.gridConfig?.rowHeight === BLOCK_SIZE) return false; - - const maxCol = Object.values(layout.components).reduce((max, comp) => { - const end = comp.position.col + comp.position.colSpan - 1; - return Math.max(max, end); - }, 0); - - return maxCol <= V5_BASE_COLUMNS; -} - -function convertV5PositionToV6( - pos: PopGridPosition, - v6DesignColumns: number, -): PopGridPosition { - const colRatio = v6DesignColumns / V5_BASE_COLUMNS; - const rowRatio = (V5_BASE_ROW_HEIGHT + V5_BASE_GAP) / (BLOCK_SIZE + BLOCK_GAP); - - const newCol = Math.max(1, Math.round((pos.col - 1) * colRatio) + 1); - let newColSpan = Math.max(1, Math.round(pos.colSpan * colRatio)); - const newRowSpan = Math.max(1, Math.round(pos.rowSpan * rowRatio)); - - if (newCol + newColSpan - 1 > v6DesignColumns) { - newColSpan = v6DesignColumns - newCol + 1; - } - - return { col: newCol, row: pos.row, colSpan: newColSpan, rowSpan: newRowSpan }; -} - -/** - * V5 레이아웃을 V6 블록 좌표로 런타임 변환 - * - 기본 모드(tablet_landscape) 좌표를 블록 단위로 변환 - * - 모드별 overrides 폐기 (자동 줄바꿈으로 대체) - * - DB 데이터는 건드리지 않음 (메모리에서만 변환) - */ -export function convertV5LayoutToV6(layout: PopLayoutDataV5): PopLayoutDataV5 { - // V5 오버라이드는 V6에서 무효 (4/6/8칸용 좌표가 13/22/31칸에 맞지 않음) - // 좌표 변환 필요 여부와 무관하게 항상 제거 - if (!isV5GridConfig(layout)) { - return { - ...layout, - gridConfig: { - rowHeight: BLOCK_SIZE, - gap: BLOCK_GAP, - padding: BLOCK_PADDING, - }, - overrides: undefined, - }; - } - - const v6Columns = getBlockColumns(V5_DESIGN_WIDTH); - - const rowGroups: Record = {}; - Object.entries(layout.components).forEach(([id, comp]) => { - const r = comp.position.row; - if (!rowGroups[r]) rowGroups[r] = []; - rowGroups[r].push(id); - }); - - const convertedPositions: Record = {}; - Object.entries(layout.components).forEach(([id, comp]) => { - convertedPositions[id] = convertV5PositionToV6(comp.position, v6Columns); - }); - - const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); - const rowMapping: Record = {}; - let v6Row = 1; - for (const v5Row of sortedRows) { - rowMapping[v5Row] = v6Row; - const maxSpan = Math.max( - ...rowGroups[v5Row].map(id => convertedPositions[id].rowSpan) - ); - v6Row += maxSpan; - } - - const newComponents = { ...layout.components }; - Object.entries(newComponents).forEach(([id, comp]) => { - const converted = convertedPositions[id]; - const mappedRow = rowMapping[comp.position.row] ?? converted.row; - newComponents[id] = { - ...comp, - position: { ...converted, row: mappedRow }, - }; - }); - - const newModals = layout.modals?.map(modal => { - const modalComps = { ...modal.components }; - Object.entries(modalComps).forEach(([id, comp]) => { - modalComps[id] = { - ...comp, - position: convertV5PositionToV6(comp.position, v6Columns), - }; - }); - return { - ...modal, - gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, - components: modalComps, - overrides: undefined, - }; - }); - - return { - ...layout, - gridConfig: { rowHeight: BLOCK_SIZE, gap: BLOCK_GAP, padding: BLOCK_PADDING }, - components: newComponents, - overrides: undefined, - modals: newModals, - }; -} diff --git a/frontend/components/pop/designer/utils/legacyLoader.ts b/frontend/components/pop/designer/utils/legacyLoader.ts new file mode 100644 index 00000000..42cf20d7 --- /dev/null +++ b/frontend/components/pop/designer/utils/legacyLoader.ts @@ -0,0 +1,128 @@ +// 레거시 레이아웃 로더 +// DB에 저장된 V5(12칸) 좌표를 현재 블록 좌표로 변환한다. +// DB 데이터는 건드리지 않고, 로드 시 메모리에서만 변환. + +import { + PopGridPosition, + PopLayoutData, + BLOCK_SIZE, + BLOCK_GAP, + BLOCK_PADDING, + getBlockColumns, +} from "../types/pop-layout"; + +const LEGACY_COLUMNS = 12; +const LEGACY_ROW_HEIGHT = 48; +const LEGACY_GAP = 16; +const DESIGN_WIDTH = 1024; + +function isLegacyGridConfig(layout: PopLayoutData): boolean { + if (layout.gridConfig?.rowHeight === BLOCK_SIZE) return false; + + const maxCol = Object.values(layout.components).reduce((max, comp) => { + const end = comp.position.col + comp.position.colSpan - 1; + return Math.max(max, end); + }, 0); + + return maxCol <= LEGACY_COLUMNS; +} + +function convertLegacyPosition( + pos: PopGridPosition, + targetColumns: number, +): PopGridPosition { + const colRatio = targetColumns / LEGACY_COLUMNS; + const rowRatio = (LEGACY_ROW_HEIGHT + LEGACY_GAP) / (BLOCK_SIZE + BLOCK_GAP); + + const newCol = Math.max(1, Math.round((pos.col - 1) * colRatio) + 1); + let newColSpan = Math.max(1, Math.round(pos.colSpan * colRatio)); + const newRowSpan = Math.max(1, Math.round(pos.rowSpan * rowRatio)); + + if (newCol + newColSpan - 1 > targetColumns) { + newColSpan = targetColumns - newCol + 1; + } + + return { col: newCol, row: pos.row, colSpan: newColSpan, rowSpan: newRowSpan }; +} + +const BLOCK_GRID_CONFIG = { + rowHeight: BLOCK_SIZE, + gap: BLOCK_GAP, + padding: BLOCK_PADDING, +}; + +/** + * DB에서 로드한 레이아웃을 현재 블록 좌표로 변환한다. + * + * - 12칸 레거시 좌표 → 블록 좌표 변환 + * - 이미 블록 좌표인 경우 → gridConfig만 보정 + * - 구 모드별 overrides는 항상 제거 (리플로우가 대체) + */ +export function loadLegacyLayout(layout: PopLayoutData): PopLayoutData { + if (!isLegacyGridConfig(layout)) { + return { + ...layout, + gridConfig: BLOCK_GRID_CONFIG, + overrides: undefined, + }; + } + + const blockColumns = getBlockColumns(DESIGN_WIDTH); + + const rowGroups: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + const r = comp.position.row; + if (!rowGroups[r]) rowGroups[r] = []; + rowGroups[r].push(id); + }); + + const convertedPositions: Record = {}; + Object.entries(layout.components).forEach(([id, comp]) => { + convertedPositions[id] = convertLegacyPosition(comp.position, blockColumns); + }); + + const sortedRows = Object.keys(rowGroups).map(Number).sort((a, b) => a - b); + const rowMapping: Record = {}; + let currentRow = 1; + for (const legacyRow of sortedRows) { + rowMapping[legacyRow] = currentRow; + const maxSpan = Math.max( + ...rowGroups[legacyRow].map(id => convertedPositions[id].rowSpan) + ); + currentRow += maxSpan; + } + + const newComponents = { ...layout.components }; + Object.entries(newComponents).forEach(([id, comp]) => { + const converted = convertedPositions[id]; + const mappedRow = rowMapping[comp.position.row] ?? converted.row; + newComponents[id] = { + ...comp, + position: { ...converted, row: mappedRow }, + }; + }); + + const newModals = layout.modals?.map(modal => { + const modalComps = { ...modal.components }; + Object.entries(modalComps).forEach(([id, comp]) => { + modalComps[id] = { + ...comp, + position: convertLegacyPosition(comp.position, blockColumns), + }; + }); + return { + ...modal, + gridConfig: BLOCK_GRID_CONFIG, + components: modalComps, + overrides: undefined, + }; + }); + + return { + ...layout, + gridConfig: BLOCK_GRID_CONFIG, + components: newComponents, + overrides: undefined, + modals: newModals, + }; +} diff --git a/frontend/components/pop/viewer/PopViewerWithModals.tsx b/frontend/components/pop/viewer/PopViewerWithModals.tsx index f322d4c0..9fbe0af5 100644 --- a/frontend/components/pop/viewer/PopViewerWithModals.tsx +++ b/frontend/components/pop/viewer/PopViewerWithModals.tsx @@ -20,7 +20,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import PopRenderer from "../designer/renderers/PopRenderer"; -import type { PopLayoutDataV5, PopModalDefinition, GridMode } from "../designer/types/pop-layout"; +import type { PopLayoutData, PopModalDefinition, GridMode } from "../designer/types/pop-layout"; import { detectGridMode, resolveModalWidth } from "../designer/types/pop-layout"; import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver"; @@ -31,7 +31,7 @@ import { useConnectionResolver } from "@/hooks/pop/useConnectionResolver"; interface PopViewerWithModalsProps { /** 전체 레이아웃 (모달 정의 포함) */ - layout: PopLayoutDataV5; + layout: PopLayoutData; /** 뷰포트 너비 */ viewportWidth: number; /** 화면 ID (이벤트 버스용) */ @@ -178,7 +178,7 @@ export default function PopViewerWithModals({ const closeOnOverlay = definition.frameConfig?.closeOnOverlay !== false; const closeOnEsc = definition.frameConfig?.closeOnEsc !== false; - const modalLayout: PopLayoutDataV5 = { + const modalLayout: PopLayoutData = { ...layout, gridConfig: definition.gridConfig, components: definition.components, diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 55829efb..6fe66e41 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -49,8 +49,8 @@ import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { useCartSync } from "@/hooks/pop/useCartSync"; import { NumberInputModal } from "../pop-card-list/NumberInputModal"; import { renderCellV2 } from "./cell-renderers"; -import type { PopLayoutDataV5 } from "@/components/pop/designer/types/pop-layout"; -import { isV5Layout, detectGridMode } from "@/components/pop/designer/types/pop-layout"; +import type { PopLayoutData } from "@/components/pop/designer/types/pop-layout"; +import { isPopLayout, detectGridMode } from "@/components/pop/designer/types/pop-layout"; import dynamic from "next/dynamic"; const PopViewerWithModals = dynamic(() => import("@/components/pop/viewer/PopViewerWithModals"), { ssr: false }); @@ -216,7 +216,7 @@ export function PopCardListV2Component({ // ===== 모달 열기 (POP 화면) ===== const [popModalOpen, setPopModalOpen] = useState(false); - const [popModalLayout, setPopModalLayout] = useState(null); + const [popModalLayout, setPopModalLayout] = useState(null); const [popModalScreenId, setPopModalScreenId] = useState(""); const [popModalRow, setPopModalRow] = useState(null); @@ -228,7 +228,7 @@ export function PopCardListV2Component({ return; } const popLayout = await screenApi.getLayoutPop(sid); - if (popLayout && isV5Layout(popLayout)) { + if (popLayout && isPopLayout(popLayout)) { setPopModalLayout(popLayout); setPopModalScreenId(String(sid)); setPopModalRow(row); diff --git a/frontend/lib/registry/pop-components/pop-scanner.tsx b/frontend/lib/registry/pop-components/pop-scanner.tsx index e2230170..4ce86cc8 100644 --- a/frontend/lib/registry/pop-components/pop-scanner.tsx +++ b/frontend/lib/registry/pop-components/pop-scanner.tsx @@ -19,7 +19,7 @@ import { usePopEvent } from "@/hooks/pop/usePopEvent"; import { BarcodeScanModal } from "@/components/common/BarcodeScanModal"; import type { PopDataConnection, - PopComponentDefinitionV5, + PopComponentDefinition, } from "@/components/pop/designer/types/pop-layout"; // ======================================== @@ -99,7 +99,7 @@ function parseScanResult( function getConnectedFields( componentId?: string, connections?: PopDataConnection[], - allComponents?: PopComponentDefinitionV5[], + allComponents?: PopComponentDefinition[], ): ConnectedFieldInfo[] { if (!componentId || !connections || !allComponents) return []; @@ -308,7 +308,7 @@ const PARSE_MODE_LABELS: Record = { interface PopScannerConfigPanelProps { config: PopScannerConfig; onUpdate: (config: PopScannerConfig) => void; - allComponents?: PopComponentDefinitionV5[]; + allComponents?: PopComponentDefinition[]; connections?: PopDataConnection[]; componentId?: string; } diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx index 8c619429..b0752146 100644 --- a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx @@ -72,7 +72,7 @@ const DEFAULT_CONFIG: PopSearchConfig = { interface ConfigPanelProps { config: PopSearchConfig | undefined; onUpdate: (config: PopSearchConfig) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -151,7 +151,7 @@ export function PopSearchConfigPanel({ config, onUpdate, allComponents, connecti interface StepProps { cfg: PopSearchConfig; update: (partial: Partial) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -268,7 +268,7 @@ interface FilterConnectionSectionProps { update: (partial: Partial) => void; showFieldName: boolean; fixedFilterMode?: SearchFilterMode; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } @@ -284,7 +284,7 @@ interface ConnectedComponentInfo { function getConnectedComponentInfo( componentId?: string, connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[], - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[], + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[], ): ConnectedComponentInfo { const empty: ConnectedComponentInfo = { tableNames: [], displayedColumns: new Set() }; if (!componentId || !connections || !allComponents) return empty; diff --git a/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx b/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx index 3b0ce864..8118dfe2 100644 --- a/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-status-bar/PopStatusBarConfig.tsx @@ -22,7 +22,7 @@ import { DEFAULT_STATUS_BAR_CONFIG, STATUS_CHIP_STYLE_LABELS } from "./types"; interface ConfigPanelProps { config: StatusBarConfig | undefined; onUpdate: (config: StatusBarConfig) => void; - allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; + allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } From 28b7f196e0f28052c1ba31fee16256115ee99839 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 13 Mar 2026 17:22:27 +0900 Subject: [PATCH 09/18] docs: add production plan management screen implementation guide - Introduced a comprehensive implementation guide for the production plan management screen, detailing the overall structure, table mappings, and V2 component capabilities. - Included specific information on the main tables used, their columns, and how they relate to the screen's functionality. - Provided an analysis of existing V2 components that can be utilized, along with those that require further development or customization. - This guide aims to facilitate the development process and ensure adherence to established standards for screen implementation. Made-with: Cursor --- .../production-plan-implementation.md | 856 ++++++++++++++++++ .../SplitPanelLayoutComponent.tsx | 5 +- .../components/v2-split-panel-layout/types.ts | 1 + 3 files changed, 859 insertions(+), 3 deletions(-) create mode 100644 docs/screen-implementation-guide/03_production/production-plan-implementation.md diff --git a/docs/screen-implementation-guide/03_production/production-plan-implementation.md b/docs/screen-implementation-guide/03_production/production-plan-implementation.md new file mode 100644 index 00000000..c487e59f --- /dev/null +++ b/docs/screen-implementation-guide/03_production/production-plan-implementation.md @@ -0,0 +1,856 @@ +# 생산계획관리 화면 구현 설계서 + +> **Screen Code**: `TOPSEAL_PP_MAIN` (screen_id: 3985) +> **메뉴 경로**: 생산관리 > 생산계획관리 +> **HTML 예시**: `00_화면개발_html/Cursor 폴더/화면개발/PC브라우저/생산/생산계획관리.html` +> **작성일**: 2026-03-13 + +--- + +## 1. 화면 전체 구조 + +``` ++---------------------------------------------------------------------+ +| 검색 섹션 (상단) | +| [품목코드] [품명] [계획기간(daterange)] [상태] | +| [사용자옵션] [엑셀업로드] [엑셀다운로드] | ++----------------------------------+--+-------------------------------+ +| 좌측 패널 (50%, 리사이즈) | | 우측 패널 (50%) | +| +------------------------------+ |리| +---------------------------+ | +| | [수주데이터] [안전재고 부족분] | |사| | [완제품] [반제품] | | +| +------------------------------+ |이| +---------------------------+ | +| | 수주 목록 헤더 | |즈| | 완제품 생산 타임라인 헤더 | | +| | [계획에없는품목만] [불러오기] | |핸| | [새로고침] [자동스케줄] | | +| | +---------------------------+| |들| | [병합] [반제품계획] [저장] | | +| | | 품목 그룹 테이블 || | | | +------------------------+| | +| | | - 품목별 그룹 행 (13컬럼) || | | | | 옵션 패널 || | +| | | -> 수주 상세 행 (7컬럼) || | | | | [리드타임] [기간] [재계산]|| | +| | | - 접기/펼치기 토글 || | | | +------------------------+| | +| | | - 체크박스 (그룹/개별) || | | | | 범례 || | +| | +---------------------------+| | | | +------------------------+| | +| +------------------------------+ | | | | 타임라인 스케줄러 || | +| | | | | (간트차트 형태) || | +| -- 안전재고 부족분 탭 -- | | | +------------------------+| | +| | 부족 품목 테이블 (8컬럼) | | | +---------------------------+ | +| | - 체크박스, 품목코드, 품명 | | | | +| | - 현재고, 안전재고, 부족수량 | | | -- 반제품 탭 -- | +| | - 권장생산량, 최종입고일 | | | | 옵션 + 안내 패널 | | +| +------------------------------+ | | | 반제품 타임라인 스케줄러 | | ++----------------------------------+--+-------------------------------+ +``` + +--- + +## 2. 사용 테이블 및 컬럼 매핑 + +### 2.1 메인 테이블 + +| 테이블명 | 용도 | PK | +|----------|------|-----| +| `production_plan_mng` | 생산계획 마스터 | `id` (serial) | +| `sales_order_mng` | 수주 데이터 (좌측 패널 조회용) | `id` (serial) | +| `item_info` | 품목 마스터 (참조) | `id` (uuid text) | +| `inventory_stock` | 재고 현황 (안전재고 부족분 탭) | `id` (uuid text) | +| `equipment_info` | 설비 정보 (타임라인 리소스) | `id` (serial) | +| `bom` / `bom_detail` | BOM 정보 (반제품 계획 생성) | `id` (uuid text) | +| `work_instruction` | 작업지시 (타임라인 연동) | 별도 확인 필요 | + +### 2.2 핵심 컬럼 매핑 - production_plan_mng + +| 컬럼명 | 타입 | 용도 | HTML 매핑 | +|--------|------|------|-----------| +| `id` | serial PK | 고유 ID | `schedule.id` | +| `company_code` | varchar | 멀티테넌시 | - | +| `plan_no` | varchar NOT NULL | 계획번호 | `SCH-{timestamp}` | +| `plan_date` | date | 계획 등록일 | 자동 | +| `item_code` | varchar NOT NULL | 품목코드 | `schedule.itemCode` | +| `item_name` | varchar | 품목명 | `schedule.itemName` | +| `product_type` | varchar | 완제품/반제품 | `'완제품'` or `'반제품'` | +| `plan_qty` | numeric NOT NULL | 계획 수량 | `schedule.quantity` | +| `completed_qty` | numeric | 완료 수량 | `schedule.completedQty` | +| `progress_rate` | numeric | 진행률(%) | `schedule.progressRate` | +| `start_date` | date NOT NULL | 시작일 | `schedule.startDate` | +| `end_date` | date NOT NULL | 종료일 | `schedule.endDate` | +| `due_date` | date | 납기일 | `schedule.dueDate` | +| `equipment_id` | integer | 설비 ID | `schedule.equipmentId` | +| `equipment_code` | varchar | 설비 코드 | - | +| `equipment_name` | varchar | 설비명 | `schedule.productionLine` | +| `status` | varchar | 상태 | `planned/in_progress/completed/work-order` | +| `priority` | varchar | 우선순위 | `normal/high/urgent` | +| `hourly_capacity` | numeric | 시간당 생산능력 | `schedule.hourlyCapacity` | +| `daily_capacity` | numeric | 일일 생산능력 | `schedule.dailyCapacity` | +| `lead_time` | integer | 리드타임(일) | `schedule.leadTime` | +| `work_shift` | varchar | 작업조 | `DAY/NIGHT/BOTH` | +| `work_order_no` | varchar | 작업지시번호 | `schedule.workOrderNo` | +| `manager_name` | varchar | 담당자 | `schedule.manager` | +| `order_no` | varchar | 연관 수주번호 | `schedule.orderInfo[].orderNo` | +| `parent_plan_id` | integer | 모 계획 ID (반제품용) | `schedule.parentPlanId` | +| `remarks` | text | 비고 | `schedule.remarks` | + +### 2.3 수주 데이터 조회용 - sales_order_mng + +| 컬럼명 | 용도 | 좌측 테이블 컬럼 매핑 | +|--------|------|----------------------| +| `order_no` | 수주번호 | 수주 상세 행 - 수주번호 | +| `part_code` | 품목코드 | 그룹 행 - 품목코드 (그룹 기준) | +| `part_name` | 품명 | 그룹 행 - 품목명 | +| `order_qty` | 수주량 | 총수주량 (SUM) | +| `ship_qty` | 출고량 | 출고량 (SUM) | +| `balance_qty` | 잔량 | 잔량 (SUM) | +| `due_date` | 납기일 | 수주 상세 행 - 납기일 | +| `partner_id` | 거래처 | 수주 상세 행 - 거래처 | +| `status` | 상태 | 상태 배지 (일반/긴급) | + +### 2.4 안전재고 부족분 조회용 - inventory_stock + item_info + +| 컬럼명 | 출처 | 좌측 테이블 컬럼 매핑 | +|--------|------|----------------------| +| `item_code` | inventory_stock | 품목코드 | +| `item_name` | item_info (JOIN) | 품목명 | +| `current_qty` | inventory_stock | 현재고 | +| `safety_qty` | inventory_stock | 안전재고 | +| `부족수량` | 계산값 (`safety_qty - current_qty`) | 부족수량 (음수면 부족) | +| `권장생산량` | 계산값 (`safety_qty * 2 - current_qty`) | 권장생산량 | +| `last_in_date` | inventory_stock | 최종입고일 | + +--- + +## 3. V2 컴포넌트 구현 가능/불가능 분석 + +### 3.1 구현 가능 (기존 V2 컴포넌트) + +| 기능 | V2 컴포넌트 | 현재 상태 | +|------|-------------|-----------| +| 좌우 분할 레이아웃 | `v2-split-panel-layout` (`displayMode: "custom"`) | layout_data에 이미 존재 | +| 검색 필터 | `v2-table-search-widget` | layout_data에 이미 존재 | +| 좌측/우측 탭 전환 | `v2-tabs-widget` | layout_data에 이미 존재 | +| 체크박스 선택 | `v2-table-grouped` (`showCheckbox: true`) | layout_data에 이미 존재 | +| 단순 그룹핑 테이블 | `v2-table-grouped` (`groupByColumn`) | layout_data에 이미 존재 | +| 타임라인 스케줄러 | `v2-timeline-scheduler` | layout_data에 이미 존재 | +| 버튼 액션 | `v2-button-primary` | layout_data에 이미 존재 | +| 안전재고 부족분 테이블 | `v2-table-list` 또는 `v2-table-grouped` | 미구성 (탭2에 컴포넌트 없음) | + +### 3.2 부분 구현 가능 (개선/확장 필요) + +| 기능 | 문제점 | 필요 작업 | +|------|--------|-----------| +| 수주 그룹 테이블 (2레벨) | `v2-table-grouped`는 **동일 컬럼 기준 그룹핑**만 지원. HTML은 그룹 행(13컬럼)과 상세 행(7컬럼)이 완전히 다른 구조 | 컴포넌트 확장 or 백엔드에서 집계 데이터를 별도 API로 제공 | +| 스케줄러 옵션 패널 | HTML의 안전리드타임/표시기간/재계산 옵션을 위한 전용 UI 없음 | `v2-input` + `v2-select` 조합으로 구성 가능 | +| 범례 UI | `v2-timeline-scheduler`에 statusColors 설정은 있지만 범례 UI 자체는 없음 | `v2-text-display` 또는 커스텀 구성 | +| 부족수량 빨간색 강조 | 조건부 서식(conditional formatting) 미지원 | 컴포넌트 확장 필요 | +| "계획에 없는 품목만" 필터 | 단순 테이블 필터가 아닌 교차 테이블 비교 필터 | 백엔드 API 필요 | + +### 3.3 신규 개발 필요 (현재 V2 컴포넌트로 불가능) + +| 기능 | 설명 | 구현 방안 | +|------|------|-----------| +| **자동 스케줄 생성 API** | 선택 품목의 필요생산계획량, 납기일, 설비 생산능력 기반으로 타임라인 자동 배치 | 백엔드 전용 API | +| **선택 계획 병합 API** | 동일 품목 복수 스케줄을 하나로 합산 | 백엔드 전용 API | +| **반제품 계획 자동 생성 API** | BOM 기반으로 완제품 계획에서 필요 반제품 소요량 계산 | 백엔드 전용 API (BOM + 재고 연계) | +| **수주 잔량/현재고 연산 조회 API** | 여러 테이블 JOIN + 집계 연산으로 좌측 패널 데이터 제공 | 백엔드 전용 API | +| **스케줄 상세 모달** | 기본정보, 근거정보, 생산정보, 계획기간, 계획분할, 설비할당 | 모달 화면 (`TOPSEAL_PP_MODAL` screen_id: 3986) 보강 | +| **설비 선택 모달** | 설비별 수량 할당 및 일정 등록 | 신규 모달 화면 필요 | +| **변경사항 확인 모달** | 자동 스케줄 생성 전후 비교 (신규/유지/삭제 건수 요약) | 신규 모달 또는 확인 다이얼로그 | + +--- + +## 4. 백엔드 API 설계 + +### 4.1 수주 데이터 조회 API (좌측 패널 - 수주데이터 탭) + +``` +GET /api/production/order-summary +``` + +**목적**: 수주 데이터를 **품목별로 그룹핑**하여 반환. 그룹 헤더에 집계값(총수주량, 출고량, 잔량, 현재고, 안전재고, 기생산계획량 등) 포함. + +**응답 구조**: +```json +{ + "success": true, + "data": [ + { + "item_code": "ITEM-001", + "item_name": "탑씰 Type A", + "hourly_capacity": 100, + "daily_capacity": 800, + "lead_time": 1, + "total_order_qty": 1000, + "total_ship_qty": 300, + "total_balance_qty": 700, + "current_stock": 100, + "safety_stock": 150, + "plan_ship_qty": 0, + "existing_plan_qty": 0, + "in_progress_qty": 0, + "required_plan_qty": 750, + "orders": [ + { + "order_no": "SO-2025-101", + "partner_name": "ABC 상사", + "order_qty": 500, + "ship_qty": 200, + "balance_qty": 300, + "due_date": "2025-11-05", + "is_urgent": false + }, + { + "order_no": "SO-2025-102", + "partner_name": "XYZ 무역", + "order_qty": 500, + "ship_qty": 100, + "balance_qty": 400, + "due_date": "2025-11-10", + "is_urgent": false + } + ] + } + ] +} +``` + +**SQL 로직 (핵심)**: +```sql +WITH order_summary AS ( + SELECT + so.part_code AS item_code, + so.part_name AS item_name, + SUM(COALESCE(so.order_qty, 0)) AS total_order_qty, + SUM(COALESCE(so.ship_qty, 0)) AS total_ship_qty, + SUM(COALESCE(so.balance_qty, 0)) AS total_balance_qty + FROM sales_order_mng so + WHERE so.company_code = $1 + AND so.status NOT IN ('cancelled', 'completed') + AND so.balance_qty > 0 + GROUP BY so.part_code, so.part_name +), +stock_info AS ( + SELECT + item_code, + SUM(COALESCE(current_qty::numeric, 0)) AS current_stock, + MAX(COALESCE(safety_qty::numeric, 0)) AS safety_stock + FROM inventory_stock + WHERE company_code = $1 + GROUP BY item_code +), +plan_info AS ( + SELECT + item_code, + SUM(CASE WHEN status = 'planned' THEN plan_qty ELSE 0 END) AS existing_plan_qty, + SUM(CASE WHEN status = 'in_progress' THEN plan_qty ELSE 0 END) AS in_progress_qty + FROM production_plan_mng + WHERE company_code = $1 + AND product_type = '완제품' + AND status NOT IN ('completed', 'cancelled') + GROUP BY item_code +) +SELECT + os.*, + COALESCE(si.current_stock, 0) AS current_stock, + COALESCE(si.safety_stock, 0) AS safety_stock, + COALESCE(pi.existing_plan_qty, 0) AS existing_plan_qty, + COALESCE(pi.in_progress_qty, 0) AS in_progress_qty, + GREATEST( + os.total_balance_qty + COALESCE(si.safety_stock, 0) - COALESCE(si.current_stock, 0) + - COALESCE(pi.existing_plan_qty, 0) - COALESCE(pi.in_progress_qty, 0), + 0 + ) AS required_plan_qty +FROM order_summary os +LEFT JOIN stock_info si ON os.item_code = si.item_code +LEFT JOIN plan_info pi ON os.item_code = pi.item_code +ORDER BY os.item_code; +``` + +**파라미터**: +- `company_code`: req.user.companyCode (자동) +- `exclude_planned` (optional): `true`이면 기존 계획이 있는 품목 제외 + +--- + +### 4.2 안전재고 부족분 조회 API (좌측 패널 - 안전재고 탭) + +``` +GET /api/production/stock-shortage +``` + +**응답 구조**: +```json +{ + "success": true, + "data": [ + { + "item_code": "ITEM-001", + "item_name": "탑씰 Type A", + "current_qty": 50, + "safety_qty": 200, + "shortage_qty": -150, + "recommended_qty": 300, + "last_in_date": "2025-10-15" + } + ] +} +``` + +**SQL 로직**: +```sql +SELECT + ist.item_code, + ii.item_name, + COALESCE(ist.current_qty::numeric, 0) AS current_qty, + COALESCE(ist.safety_qty::numeric, 0) AS safety_qty, + (COALESCE(ist.current_qty::numeric, 0) - COALESCE(ist.safety_qty::numeric, 0)) AS shortage_qty, + GREATEST(COALESCE(ist.safety_qty::numeric, 0) * 2 - COALESCE(ist.current_qty::numeric, 0), 0) AS recommended_qty, + ist.last_in_date +FROM inventory_stock ist +JOIN item_info ii ON ist.item_code = ii.id AND ist.company_code = ii.company_code +WHERE ist.company_code = $1 + AND COALESCE(ist.current_qty::numeric, 0) < COALESCE(ist.safety_qty::numeric, 0) +ORDER BY shortage_qty ASC; +``` + +--- + +### 4.3 자동 스케줄 생성 API + +``` +POST /api/production/generate-schedule +``` + +**요청 body**: +```json +{ + "items": [ + { + "item_code": "ITEM-001", + "item_name": "탑씰 Type A", + "required_qty": 750, + "earliest_due_date": "2025-11-05", + "hourly_capacity": 100, + "daily_capacity": 800, + "lead_time": 1, + "orders": [ + { "order_no": "SO-2025-101", "balance_qty": 300, "due_date": "2025-11-05" }, + { "order_no": "SO-2025-102", "balance_qty": 400, "due_date": "2025-11-10" } + ] + } + ], + "options": { + "safety_lead_time": 1, + "recalculate_unstarted": true, + "product_type": "완제품" + } +} +``` + +**비즈니스 로직**: +1. 각 품목의 필요생산계획량, 납기일, 일일생산능력을 기반으로 생산일수 계산 +2. `생산일수 = ceil(필요생산계획량 / 일일생산능력)` +3. `시작일 = 납기일 - 생산일수 - 안전리드타임` +4. 시작일이 오늘 이전이면 오늘로 조정 +5. `recalculate_unstarted = true`면 기존 진행중/작업지시/완료 스케줄은 유지, 미진행(planned)만 제거 후 재계산 +6. 결과를 `production_plan_mng`에 INSERT +7. 변경사항 요약(신규/유지/삭제 건수) 반환 + +**응답 구조**: +```json +{ + "success": true, + "data": { + "summary": { + "total": 3, + "new_count": 2, + "kept_count": 1, + "deleted_count": 1 + }, + "schedules": [ + { + "id": 101, + "plan_no": "PP-2025-0001", + "item_code": "ITEM-001", + "item_name": "탑씰 Type A", + "plan_qty": 750, + "start_date": "2025-10-30", + "end_date": "2025-11-03", + "due_date": "2025-11-05", + "status": "planned" + } + ] + } +} +``` + +--- + +### 4.4 스케줄 병합 API + +``` +POST /api/production/merge-schedules +``` + +**요청 body**: +```json +{ + "schedule_ids": [101, 102, 103], + "product_type": "완제품" +} +``` + +**비즈니스 로직**: +1. 선택된 스케줄이 모두 동일 품목인지 검증 +2. 완제품/반제품이 섞여있지 않은지 검증 +3. 수량 합산, 가장 빠른 시작일/납기일, 가장 늦은 종료일 적용 +4. 원본 스케줄 DELETE, 병합된 스케줄 INSERT +5. 수주 정보(order_no)는 병합 (중복 제거) + +--- + +### 4.5 반제품 계획 자동 생성 API + +``` +POST /api/production/generate-semi-schedule +``` + +**요청 body**: +```json +{ + "plan_ids": [101, 102], + "options": { + "consider_stock": true, + "keep_in_progress": false, + "exclude_used": true + } +} +``` + +**비즈니스 로직**: +1. 선택된 완제품 계획의 품목코드로 BOM 조회 +2. `bom` 테이블에서 해당 품목의 `item_id` → `bom_detail`에서 하위 반제품(`child_item_id`) 조회 +3. 각 반제품의 필요 수량 = `완제품 계획수량 x BOM 소요량(quantity)` +4. `consider_stock = true`면 현재고/안전재고 감안하여 순 필요량 계산 +5. `exclude_used = true`면 이미 투입된 반제품 수량 차감 +6. 모품목 생산 시작일 고려하여 반제품 납기일 설정 (시작일 - 반제품 리드타임) +7. `production_plan_mng`에 `product_type = '반제품'`, `parent_plan_id` 설정하여 INSERT + +--- + +### 4.6 스케줄 상세 저장/수정 API + +``` +PUT /api/production/plan/:id +``` + +**요청 body**: +```json +{ + "plan_qty": 750, + "start_date": "2025-10-30", + "end_date": "2025-11-03", + "equipment_id": 1, + "equipment_code": "LINE-01", + "equipment_name": "1호기", + "manager_name": "홍길동", + "work_shift": "DAY", + "priority": "high", + "remarks": "긴급 생산" +} +``` + +--- + +### 4.7 스케줄 분할 API + +``` +POST /api/production/split-schedule +``` + +**요청 body**: +```json +{ + "plan_id": 101, + "splits": [ + { "qty": 500, "start_date": "2025-10-30", "end_date": "2025-11-01" }, + { "qty": 250, "start_date": "2025-11-02", "end_date": "2025-11-03" } + ] +} +``` + +**비즈니스 로직**: +1. 분할 수량 합산이 원본 수량과 일치하는지 검증 +2. 원본 스케줄 DELETE +3. 분할된 각 조각을 신규 INSERT (동일 `order_no`, `item_code` 유지) + +--- + +## 5. 모달 화면 설계 + +### 5.1 스케줄 상세 모달 (screen_id: 3986 보강) + +**섹션 구성**: + +| 섹션 | 필드 | 타입 | 비고 | +|------|------|------|------| +| **기본 정보** | 품목코드, 품목명 | text (readonly) | 자동 채움 | +| **근거 정보** | 수주번호/거래처/납기일 목록 | text (readonly) | 연관 수주 정보 표시 | +| **생산 정보** | 총 생산수량 | number | 수정 가능 | +| | 납기일 (수주 기준) | date (readonly) | 가장 빠른 납기일 | +| **계획 기간** | 계획 시작일, 종료일 | date | 수정 가능 | +| | 생산 기간 | text (readonly) | 자동 계산 표시 | +| **계획 분할** | 분할 개수, 분할 수량 입력 | select, number | 분할하기 기능 | +| **설비 할당** | 설비 선택 버튼 | button → 모달 | 설비 선택 모달 오픈 | +| **생산 상태** | 상태 | select (disabled) | `planned/work-order/in_progress/completed` | +| **추가 정보** | 담당자, 작업지시번호, 비고 | text | 수정 가능 | +| **하단 버튼** | 삭제, 취소, 저장 | buttons | - | + +### 5.2 수주 불러오기 모달 + +**구성**: +- 선택된 품목 목록 표시 +- 주의사항 안내 +- 라디오 버튼: "기존 계획에 추가" / "별도 계획으로 생성" +- 취소/불러오기 버튼 + +### 5.3 안전재고 불러오기 모달 + +**구성**: 수주 불러오기 모달과 동일한 패턴 + +### 5.4 설비 선택 모달 + +**구성**: +- 총 수량 / 할당 수량 / 미할당 수량 요약 +- 설비 카드 그리드 (설비명, 생산능력, 할당 수량 입력, 시작일/종료일) +- 취소/저장 버튼 + +### 5.5 변경사항 확인 모달 + +**구성**: +- 경고 메시지 +- 변경사항 요약 카드 (총 계획, 신규 생성, 유지됨, 삭제됨) +- 변경사항 상세 목록 (품목별 변경 전/후 비교) +- 취소/확인 및 적용 버튼 + +--- + +## 6. 현재 layout_data 수정 필요 사항 + +### 6.1 현재 layout_data 구조 (screen_id: 3985, layout_id: 9192) + +``` +comp_search (v2-table-search-widget) - 검색 필터 +comp_split_panel (v2-split-panel-layout) + ├── leftPanel (custom mode) + │ ├── left_tabs (v2-tabs-widget) - [수주데이터, 안전재고 부족분] + │ ├── order_table (v2-table-grouped) - 수주 테이블 + │ └── btn_import (v2-button-primary) - 선택 품목 불러오기 + ├── rightPanel (custom mode) + │ ├── right_tabs (v2-tabs-widget) - [완제품, 반제품] + │ │ └── finished_tab.components + │ │ ├── v2-timeline-scheduler - 타임라인 + │ │ └── v2-button-primary - 스케줄 생성 + │ ├── btn_save (v2-button-primary) - 자동 스케줄 생성 + │ └── btn_clear (v2-button-primary) - 초기화 +comp_q0iqzkpx (v2-button-primary) - 하단 저장 버튼 (무의미) +``` + +### 6.2 수정 필요 사항 + +| 항목 | 현재 상태 | 필요 상태 | +|------|-----------|-----------| +| **좌측 - 안전재고 탭** | 컴포넌트 없음 (`"컴포넌트가 없습니다"` 표시) | `v2-table-list` 또는 별도 조회 API 연결된 테이블 추가 | +| **좌측 - order_table** | `selectedTable: "sales_order_mng"` (범용 API) | 전용 API (`/api/production/order-summary`)로 변경 필요 | +| **좌측 - 체크박스 필터** | 없음 | "계획에 없는 품목만" 체크박스 UI 추가 | +| **우측 - 반제품 탭** | 컴포넌트 없음 | 반제품 타임라인 + 옵션 패널 추가 | +| **우측 - 타임라인** | `selectedTable: "work_instruction"` | `selectedTable: "production_plan_mng"` + 필터 `product_type='완제품'` | +| **우측 - 옵션 패널** | 없음 | 안전리드타임, 표시기간, 재계산 체크박스 → `v2-input` 조합 | +| **우측 - 범례** | 없음 | `v2-text-display` 또는 커스텀 범례 컴포넌트 | +| **우측 - 버튼들** | 일부만 존재 | 병합, 반제품계획, 저장, 초기화 추가 | +| **하단 저장 버튼** | 존재 (무의미) | 제거 | +| **우측 패널 렌더링 버그** | 타임라인 미렌더링 | SplitPanelLayout custom 모드 디버깅 필요 | + +--- + +## 7. 구현 단계별 계획 + +### Phase 1: 기존 버그 수정 + 기본 구조 안정화 + +**목표**: 현재 layout_data로 화면이 최소한 정상 렌더링되게 만들기 + +| 작업 | 상세 | 예상 난이도 | +|------|------|-------------| +| 1-1. 좌측 z-index 겹침 수정 | SplitPanelLayout의 custom 모드에서 내부 컴포넌트가 비대화형 div에 가려지는 이슈 | 중 | +| 1-2. 우측 타임라인 렌더링 수정 | tabs-widget 내부 timeline-scheduler가 렌더링되지 않는 이슈 | 중 | +| 1-3. 하단 저장 버튼 제거 | layout_data에서 `comp_q0iqzkpx` 제거 | 하 | +| 1-4. 타임라인 데이터 소스 수정 | `work_instruction` → `production_plan_mng`으로 변경 | 하 | + +### Phase 2: 백엔드 API 개발 + +**목표**: 화면에 필요한 데이터를 제공하는 전용 API 구축 + +| 작업 | 상세 | 예상 난이도 | +|------|------|-------------| +| 2-1. 수주 데이터 조회 API | `GET /api/production/order-summary` (4.1 참조) | 중 | +| 2-2. 안전재고 부족분 API | `GET /api/production/stock-shortage` (4.2 참조) | 하 | +| 2-3. 자동 스케줄 생성 API | `POST /api/production/generate-schedule` (4.3 참조) | 상 | +| 2-4. 스케줄 CRUD API | `PUT/DELETE /api/production/plan/:id` (4.6 참조) | 중 | +| 2-5. 스케줄 병합 API | `POST /api/production/merge-schedules` (4.4 참조) | 중 | +| 2-6. 반제품 계획 자동 생성 API | `POST /api/production/generate-semi-schedule` (4.5 참조) | 상 | +| 2-7. 스케줄 분할 API | `POST /api/production/split-schedule` (4.7 참조) | 중 | + +### Phase 3: layout_data 보강 + 모달 화면 + +**목표**: 안전재고 탭, 반제품 탭, 모달들 구성 + +| 작업 | 상세 | 예상 난이도 | +|------|------|-------------| +| 3-1. 안전재고 부족분 탭 구성 | `stock_tab`에 테이블 컴포넌트 + "선택 품목 불러오기" 버튼 추가 | 중 | +| 3-2. 반제품 탭 구성 | `semi_tab`에 타임라인 + 옵션 + 버튼 추가 | 중 | +| 3-3. 옵션 패널 구성 | v2-input 조합으로 안전리드타임, 표시기간, 체크박스 | 중 | +| 3-4. 버튼 액션 연결 | 자동 스케줄, 병합, 반제품계획, 저장, 초기화 → API 연결 | 중 | +| 3-5. 스케줄 상세 모달 보강 | screen_id: 3986 layout_data 수정 | 중 | +| 3-6. 수주/안전재고 불러오기 모달 | 신규 모달 screen 생성 | 중 | +| 3-7. 설비 선택 모달 | 신규 모달 screen 생성 | 중 | + +### Phase 4: v2-table-grouped 확장 (2레벨 트리 지원) + +**목표**: HTML 예시의 "품목 그룹 → 수주 상세" 2레벨 트리 테이블 구현 + +| 작업 | 상세 | 예상 난이도 | +|------|------|-------------| +| 4-1. 컴포넌트 확장 설계 | 그룹 행과 상세 행이 다른 컬럼 구조를 가질 수 있도록 설계 | 상 | +| 4-2. expandedRowRenderer 구현 | 그룹 행 펼침 시 별도 컬럼/데이터로 하위 행 렌더링 | 상 | +| 4-3. 그룹 행 집계 컬럼 설정 | 그룹 헤더에 SUM, 계산 필드 표시 (현재고, 안전재고, 필요생산계획 등) | 중 | +| 4-4. 조건부 서식 지원 | 부족수량 빨간색, 양수 초록색 등 | 중 | + +**대안**: Phase 4가 너무 복잡하면, 좌측 수주데이터를 2개 연동 테이블로 분리 (상단: 품목별 집계 테이블, 하단: 선택 품목의 수주 상세 테이블) 하는 방식도 검토 가능 + +--- + +## 8. 파일 생성/수정 목록 + +### 8.1 백엔드 + +| 파일 | 작업 | 비고 | +|------|------|------| +| `backend-node/src/routes/productionRoutes.ts` | 라우터 등록 | 신규 or 기존 확장 | +| `backend-node/src/controllers/productionController.ts` | API 핸들러 | 신규 or 기존 확장 | +| `backend-node/src/services/productionPlanService.ts` | 비즈니스 로직 서비스 | 신규 | + +### 8.2 DB (layout_data 수정) + +| 대상 | 작업 | +|------|------| +| `screen_layouts_v2` (screen_id: 3985) | layout_data JSON 수정 | +| `screen_layouts_v2` (screen_id: 3986) | 모달 layout_data 보강 | +| `screen_definitions` + `screen_layouts_v2` | 설비 선택 모달 신규 등록 | +| `screen_definitions` + `screen_layouts_v2` | 불러오기 모달 신규 등록 | + +### 8.3 프론트엔드 (API 클라이언트) + +| 파일 | 작업 | +|------|------| +| `frontend/lib/api/production.ts` | 생산계획 전용 API 클라이언트 함수 추가 | + +### 8.4 프론트엔드 (V2 컴포넌트 확장, Phase 4) + +| 파일 | 작업 | +|------|------| +| `frontend/lib/registry/components/v2-table-grouped/` | 2레벨 트리 지원 확장 | +| `frontend/lib/registry/components/v2-timeline-scheduler/` | 옵션 패널/범례 확장 (필요시) | + +--- + +## 9. 이벤트 흐름 (주요 시나리오) + +### 9.1 자동 스케줄 생성 흐름 + +``` +1. 사용자가 좌측 수주데이터에서 품목 체크박스 선택 +2. 우측 "자동 스케줄 생성" 버튼 클릭 +3. (옵션 확인) 안전리드타임, 재계산 모드 체크 +4. POST /api/production/generate-schedule 호출 +5. (응답) 변경사항 확인 모달 표시 (신규/유지/삭제 건수) +6. 사용자 "확인 및 적용" 클릭 +7. 타임라인 스케줄러 새로고침 +8. 좌측 수주 목록의 "기생산계획량" 컬럼 갱신 +``` + +### 9.2 수주 불러오기 흐름 + +``` +1. 사용자가 좌측 수주데이터에서 품목 체크박스 선택 +2. "선택 품목 불러오기" 버튼 클릭 +3. 불러오기 모달 표시 (선택 품목 목록 + 추가방식 선택) +4. "기존 계획에 추가" or "별도 계획으로 생성" 선택 +5. "불러오기" 버튼 클릭 +6. POST /api/production/generate-schedule 호출 (단건) +7. 타임라인 새로고침 +``` + +### 9.3 타임라인 스케줄 클릭 → 상세 모달 + +``` +1. 사용자가 타임라인의 스케줄 바 클릭 +2. 스케줄 상세 모달 오픈 (TOPSEAL_PP_MODAL) +3. 기본정보(readonly), 근거정보(readonly), 생산정보(수정가능) 표시 +4. 계획기간 수정, 설비할당, 분할 등 작업 +5. "저장" → PUT /api/production/plan/:id +6. "삭제" → DELETE /api/production/plan/:id +7. 모달 닫기 → 타임라인 새로고침 +``` + +### 9.4 반제품 계획 생성 흐름 + +``` +1. 우측 완제품 탭에서 스케줄 체크박스 선택 +2. "선택 품목 → 반제품 계획" 버튼 클릭 +3. POST /api/production/generate-semi-schedule 호출 + - BOM 조회 → 필요 반제품 목록 + 소요량 계산 + - 재고 감안 → 순 필요량 계산 + - 반제품 계획 INSERT (product_type='반제품', parent_plan_id 설정) +4. 반제품 탭으로 자동 전환 +5. 반제품 타임라인 새로고침 +``` + +--- + +## 10. 검색 필드 설정 + +| 필드명 | 타입 | 라벨 | 대상 컬럼 | +|--------|------|------|-----------| +| `item_code` | text | 품목코드 | `part_code` (수주) / `item_code` (계획) | +| `item_name` | text | 품명 | `part_name` / `item_name` | +| `plan_date` | daterange | 계획기간 | `start_date` ~ `end_date` | +| `status` | select | 상태 | 전체 / 계획 / 진행 / 완료 | + +--- + +## 11. 권한 및 멀티테넌시 + +### 11.1 모든 API에 적용 + +```typescript +const companyCode = req.user!.companyCode; + +if (companyCode === '*') { + // 최고관리자: 모든 회사 데이터 조회 가능 +} else { + // 일반 회사: WHERE company_code = $1 필수 +} +``` + +### 11.2 데이터 격리 + +- `production_plan_mng.company_code` 필터 필수 +- `sales_order_mng.company_code` 필터 필수 +- `inventory_stock.company_code` 필터 필수 +- JOIN 시 양쪽 테이블 모두 `company_code` 조건 포함 + +--- + +## 12. 우선순위 정리 + +| 우선순위 | 작업 | 이유 | +|----------|------|------| +| **1 (긴급)** | Phase 1: 기존 렌더링 버그 수정 | 현재 화면 자체가 정상 동작하지 않음 | +| **2 (높음)** | Phase 2-1, 2-2: 수주/재고 조회 API | 좌측 패널의 핵심 데이터 | +| **3 (높음)** | Phase 2-3: 자동 스케줄 생성 API | 우측 패널의 핵심 기능 | +| **4 (중간)** | Phase 3: layout_data 보강 | 안전재고 탭, 반제품 탭, 모달 | +| **5 (중간)** | Phase 2-4~2-7: 나머지 API | 병합, 분할, 반제품 계획 | +| **6 (낮음)** | Phase 4: 2레벨 트리 테이블 확장 | 현재 단순 그룹핑으로도 기본 동작 | + +--- + +## 부록 A: HTML 예시의 모달 목록 + +| 모달명 | HTML ID | 용도 | +|--------|---------|------| +| 스케줄 상세 모달 | `scheduleModal` | 스케줄 기본정보/근거정보/생산정보/계획기간/분할/설비할당/상태/추가정보 | +| 수주 불러오기 모달 | `orderImportModal` | 선택 품목 목록 + 추가방식 선택 (기존추가/별도생성) | +| 안전재고 불러오기 모달 | `stockImportModal` | 부족 품목 목록 + 추가방식 선택 | +| 설비 선택 모달 | `equipmentSelectModal` | 설비 카드 + 수량할당 + 일정등록 | +| 변경사항 확인 모달 | `changeConfirmModal` | 자동스케줄 생성 결과 요약 + 상세 비교 | + +## 부록 B: HTML 예시의 JS 핵심 함수 목록 + +| 함수명 | 기능 | 매핑 API | +|--------|------|----------| +| `generateSchedule()` | 자동 스케줄 생성 (품목별 합산) | POST /api/production/generate-schedule | +| `saveSchedule()` | 스케줄 저장 (localStorage → DB) | POST /api/production/plan (bulk) | +| `mergeSelectedSchedules()` | 선택 계획 병합 | POST /api/production/merge-schedules | +| `generateSemiFromSelected()` | 반제품 계획 자동 생성 | POST /api/production/generate-semi-schedule | +| `saveScheduleFromModal()` | 모달에서 스케줄 저장 | PUT /api/production/plan/:id | +| `deleteScheduleFromModal()` | 모달에서 스케줄 삭제 | DELETE /api/production/plan/:id | +| `openOrderImportModal()` | 수주 불러오기 모달 열기 | - (프론트엔드 UI) | +| `importOrderItems()` | 수주 품목 불러오기 실행 | POST /api/production/generate-schedule | +| `openStockImportModal()` | 안전재고 불러오기 모달 열기 | - (프론트엔드 UI) | +| `importStockItems()` | 안전재고 품목 불러오기 실행 | POST /api/production/generate-schedule | +| `refreshOrderList()` | 수주 목록 새로고침 | GET /api/production/order-summary | +| `refreshStockList()` | 재고 부족 목록 새로고침 | GET /api/production/stock-shortage | +| `switchTab(tabName)` | 좌측 탭 전환 | - (프론트엔드 UI) | +| `switchTimelineTab(tabName)` | 우측 탭 전환 | - (프론트엔드 UI) | +| `toggleOrderDetails(itemGroup)` | 품목 그룹 펼치기/접기 | - (프론트엔드 UI) | +| `renderTimeline()` | 완제품 타임라인 렌더링 | - (프론트엔드 UI) | +| `renderSemiTimeline()` | 반제품 타임라인 렌더링 | - (프론트엔드 UI) | +| `executeSplit()` | 계획 분할 실행 | POST /api/production/split-schedule | +| `openEquipmentSelectModal()` | 설비 선택 모달 열기 | GET /api/equipment (기존) | +| `saveEquipmentSelection()` | 설비 할당 저장 | PUT /api/production/plan/:id | +| `applyScheduleChanges()` | 변경사항 확인 후 적용 | - (프론트엔드 상태 관리) | + +## 부록 C: 수주 데이터 테이블 컬럼 상세 + +### 그룹 행 (품목별 집계) + +| # | 컬럼 | 데이터 소스 | 정렬 | +|---|------|-------------|------| +| 1 | 체크박스 | - | center | +| 2 | 토글 (펼치기/접기) | - | center | +| 3 | 품목코드 | `sales_order_mng.part_code` (GROUP BY) | left | +| 4 | 품목명 | `sales_order_mng.part_name` | left | +| 5 | 총수주량 | `SUM(order_qty)` | right | +| 6 | 출고량 | `SUM(ship_qty)` | right | +| 7 | 잔량 | `SUM(balance_qty)` | right | +| 8 | 현재고 | `inventory_stock.current_qty` (JOIN) | right | +| 9 | 안전재고 | `inventory_stock.safety_qty` (JOIN) | right | +| 10 | 출하계획량 | `SUM(plan_ship_qty)` | right | +| 11 | 기생산계획량 | `production_plan_mng` 조회 (JOIN) | right | +| 12 | 생산진행 | `production_plan_mng` (status='in_progress') 조회 | right | +| 13 | 필요생산계획 | 계산값 (잔량+안전재고-현재고-기생산계획량-생산진행) | right, 빨간색 강조 | + +### 상세 행 (개별 수주) + +| # | 컬럼 | 데이터 소스 | +|---|------|-------------| +| 1 | (빈 칸) | - | +| 2 | (빈 칸) | - | +| 3-4 | 수주번호, 거래처, 상태배지 | `order_no`, `partner_id` → partner_name, `status` | +| 5 | 수주량 | `order_qty` | +| 6 | 출고량 | `ship_qty` | +| 7 | 잔량 | `balance_qty` | +| 8-13 | 납기일 (colspan) | `due_date` | + +## 부록 D: 타임라인 스케줄러 필드 매핑 + +### 완제품 타임라인 + +| 타임라인 필드 | production_plan_mng 컬럼 | 비고 | +|--------------|--------------------------|------| +| `id` | `id` | PK | +| `resourceId` | `item_code` | 품목 기준 리소스 (설비 기준이 아님) | +| `title` | `item_name` + `plan_qty` | 표시 텍스트 | +| `startDate` | `start_date` | 시작일 | +| `endDate` | `end_date` | 종료일 | +| `status` | `status` | planned/in_progress/completed/work-order | +| `progress` | `progress_rate` | 진행률(%) | + +### 반제품 타임라인 + +동일 구조, 단 `product_type = '반제품'` 필터 적용 + +### statusColors 매핑 + +| 상태 | 색상 | 의미 | +|------|------|------| +| `planned` | `#3b82f6` (파란색) | 계획됨 | +| `work-order` | `#f59e0b` (노란색) | 작업지시 | +| `in_progress` | `#10b981` (초록색) | 진행중 | +| `completed` | `#6b7280` (회색, 반투명) | 완료 | +| `delayed` | `#ef4444` (빨간색) | 지연 | diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 0f54b4f2..97ffef4e 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -4384,9 +4384,8 @@ export const SplitPanelLayoutComponent: React.FC } })() ) : componentConfig.rightPanel?.displayMode === "custom" ? ( - // 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 - // 실행 모드에서 좌측 미선택 시 안내 메시지 표시 - !isDesignMode && !selectedLeftItem ? ( + // 커스텀 모드: alwaysShow가 아닌 경우에만 좌측 선택 필요 + !isDesignMode && !selectedLeftItem && !componentConfig.rightPanel?.alwaysShow ? (

좌측에서 항목을 선택하세요

diff --git a/frontend/lib/registry/components/v2-split-panel-layout/types.ts b/frontend/lib/registry/components/v2-split-panel-layout/types.ts index ed41f578..5b87a82e 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/types.ts +++ b/frontend/lib/registry/components/v2-split-panel-layout/types.ts @@ -237,6 +237,7 @@ export interface SplitPanelLayoutConfig { customTableName?: string; // 사용자 지정 테이블명 (useCustomTable이 true일 때) dataSource?: string; displayMode?: "list" | "table" | "custom"; // 표시 모드: 목록, 테이블, 또는 커스텀 + alwaysShow?: boolean; // true면 좌측 선택 없이도 우측 패널 항상 표시 // 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치 (탭 컴포넌트와 동일 구조) components?: PanelInlineComponent[]; showSearch?: boolean; From 8c12caf936c79578d02c4de5756f88f5c76a5e39 Mon Sep 17 00:00:00 2001 From: kmh Date: Mon, 16 Mar 2026 09:26:04 +0900 Subject: [PATCH 10/18] chore: update .gitignore and remove unused images - Added support for ignoring PNG files in .gitignore to streamline file management. - Deleted unused image files from the .playwright-mcp directory to reduce clutter and improve project organization. - Enhanced column visibility handling in TableListComponent to include width adjustments and localStorage synchronization for better user experience. Made-with: Cursor --- frontend/app/globals.css | 15 +++------------ .../SplitPanelLayoutComponent.tsx | 6 +++--- .../SplitPanelLayoutComponent.tsx | 14 +++++++------- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b3dbab89..b9173da6 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -402,18 +402,9 @@ select { /* 필요시 특정 컴포넌트에 대한 스타일 오버라이드를 여기에 추가 */ /* 예: Calendar, Table 등의 미세 조정 */ -/* 모바일에서 테이블 레이아웃 고정 (화면 밖으로 넘어가지 않도록) */ -@media (max-width: 639px) { - .table-mobile-fixed { - table-layout: fixed; - } -} - -/* 데스크톱에서 테이블 레이아웃 자동 (기본값이지만 명시적으로 설정) */ -@media (min-width: 640px) { - .table-mobile-fixed { - table-layout: auto; - } +/* 테이블 레이아웃 고정 (셀 내용이 영역을 벗어나지 않도록) */ +.table-mobile-fixed { + table-layout: fixed; } /* 그리드선 숨기기 */ diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index b5e8852f..597c759a 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -2670,7 +2670,7 @@ export const SplitPanelLayoutComponent: React.FC {formatCellValue( col.name, @@ -2732,7 +2732,7 @@ export const SplitPanelLayoutComponent: React.FC {formatCellValue( col.name, @@ -3415,7 +3415,7 @@ export const SplitPanelLayoutComponent: React.FC {formatCellValue( col.name, diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 9ed4c81f..41fa815e 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -3603,7 +3603,7 @@ export const SplitPanelLayoutComponent: React.FC {formatCellValue( col.name, @@ -3700,7 +3700,7 @@ export const SplitPanelLayoutComponent: React.FC {formatCellValue( col.name, @@ -4197,7 +4197,7 @@ export const SplitPanelLayoutComponent: React.FC onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > {tabSummaryColumns.map((col: any) => ( - + {col.type === "progress" ? renderProgressCell(col, item, selectedLeftItem) : formatCellValue( @@ -4313,7 +4313,7 @@ export const SplitPanelLayoutComponent: React.FC onClick={() => toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > {listSummaryColumns.map((col: any) => ( - + {col.type === "progress" ? renderProgressCell(col, item, selectedLeftItem) : formatCellValue( @@ -4706,8 +4706,8 @@ export const SplitPanelLayoutComponent: React.FC {columnsToShow.map((col, colIdx) => ( {col.type === "progress" ? renderProgressCell(col, item, selectedLeftItem) @@ -4853,7 +4853,7 @@ export const SplitPanelLayoutComponent: React.FC onClick={() => toggleRightItemExpansion(itemId)} > {columnsToDisplay.map((col) => ( - + {formatCellValue( col.name, getEntityJoinValue(item, col.name), From 17a5d2ff9b39a7e26a054b5e46d1ee9bb8dbf6c2 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 16 Mar 2026 09:28:22 +0900 Subject: [PATCH 11/18] feat: implement production plan management functionality - Added production plan management routes and controller to handle various operations including order summary retrieval, stock shortage checks, and CRUD operations for production plans. - Introduced service layer for production plan management, encapsulating business logic for handling production-related data. - Created API client for production plan management, enabling frontend interaction with the new backend endpoints. - Enhanced button actions to support API calls for production scheduling and management tasks. These changes aim to improve the management of production plans, enhancing usability and functionality within the ERP system. Made-with: Cursor --- backend-node/src/app.ts | 2 + .../src/controllers/productionController.ts | 190 +++++ backend-node/src/routes/productionRoutes.ts | 36 + .../src/services/productionPlanService.ts | 668 ++++++++++++++++++ frontend/lib/api/production.ts | 178 +++++ .../hooks/useTimelineData.ts | 9 +- .../components/v2-timeline-scheduler/types.ts | 3 + frontend/lib/utils/buttonActions.ts | 113 ++- 8 files changed, 1197 insertions(+), 2 deletions(-) create mode 100644 backend-node/src/controllers/productionController.ts create mode 100644 backend-node/src/routes/productionRoutes.ts create mode 100644 backend-node/src/services/productionPlanService.ts create mode 100644 frontend/lib/api/production.ts diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index f45a88cd..206900bf 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -113,6 +113,7 @@ import scheduleRoutes from "./routes/scheduleRoutes"; // 스케줄 자동 생성 import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리 import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변경 이력 조회 import bomRoutes from "./routes/bomRoutes"; // BOM 이력/버전 관리 +import productionRoutes from "./routes/productionRoutes"; // 생산계획 관리 import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리 import departmentRoutes from "./routes/departmentRoutes"; // 부서 관리 import tableCategoryValueRoutes from "./routes/tableCategoryValueRoutes"; // 카테고리 값 관리 @@ -310,6 +311,7 @@ app.use("/api/schedule", scheduleRoutes); // 스케줄 자동 생성 app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리 app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력 조회 app.use("/api/bom", bomRoutes); // BOM 이력/버전 관리 +app.use("/api/production", productionRoutes); // 생산계획 관리 app.use("/api/roles", roleRoutes); // 권한 그룹 관리 app.use("/api/departments", departmentRoutes); // 부서 관리 app.use("/api/table-categories", tableCategoryValueRoutes); // 카테고리 값 관리 diff --git a/backend-node/src/controllers/productionController.ts b/backend-node/src/controllers/productionController.ts new file mode 100644 index 00000000..9d6c56c4 --- /dev/null +++ b/backend-node/src/controllers/productionController.ts @@ -0,0 +1,190 @@ +/** + * 생산계획 컨트롤러 + */ + +import { Request, Response } from "express"; +import * as productionService from "../services/productionPlanService"; +import { logger } from "../utils/logger"; + +// ─── 수주 데이터 조회 (품목별 그룹핑) ─── + +export async function getOrderSummary(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { excludePlanned, itemCode, itemName } = req.query; + + const data = await productionService.getOrderSummary(companyCode, { + excludePlanned: excludePlanned === "true", + itemCode: itemCode as string, + itemName: itemName as string, + }); + + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("수주 데이터 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// ─── 안전재고 부족분 조회 ─── + +export async function getStockShortage(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const data = await productionService.getStockShortage(companyCode); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("안전재고 부족분 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// ─── 생산계획 상세 조회 ─── + +export async function getPlanById(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const planId = parseInt(req.params.id, 10); + const data = await productionService.getPlanById(companyCode, planId); + + if (!data) { + return res.status(404).json({ success: false, message: "생산계획을 찾을 수 없습니다" }); + } + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("생산계획 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// ─── 생산계획 수정 ─── + +export async function updatePlan(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const planId = parseInt(req.params.id, 10); + const updatedBy = req.user!.userId; + + const data = await productionService.updatePlan(companyCode, planId, req.body, updatedBy); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("생산계획 수정 실패", { error: error.message }); + return res.status(error.message.includes("찾을 수 없") ? 404 : 500).json({ + success: false, + message: error.message, + }); + } +} + +// ─── 생산계획 삭제 ─── + +export async function deletePlan(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const planId = parseInt(req.params.id, 10); + + await productionService.deletePlan(companyCode, planId); + return res.json({ success: true, message: "삭제되었습니다" }); + } catch (error: any) { + logger.error("생산계획 삭제 실패", { error: error.message }); + return res.status(error.message.includes("찾을 수 없") ? 404 : 500).json({ + success: false, + message: error.message, + }); + } +} + +// ─── 자동 스케줄 생성 ─── + +export async function generateSchedule(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const createdBy = req.user!.userId; + const { items, options } = req.body; + + if (!items || !Array.isArray(items) || items.length === 0) { + return res.status(400).json({ success: false, message: "품목 정보가 필요합니다" }); + } + + const data = await productionService.generateSchedule(companyCode, items, options || {}, createdBy); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("자동 스케줄 생성 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// ─── 스케줄 병합 ─── + +export async function mergeSchedules(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const mergedBy = req.user!.userId; + const { schedule_ids, product_type } = req.body; + + if (!schedule_ids || !Array.isArray(schedule_ids) || schedule_ids.length < 2) { + return res.status(400).json({ success: false, message: "2개 이상의 스케줄을 선택해주세요" }); + } + + const data = await productionService.mergeSchedules( + companyCode, + schedule_ids, + product_type || "완제품", + mergedBy + ); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("스케줄 병합 실패", { error: error.message }); + const status = error.message.includes("동일 품목") || error.message.includes("찾을 수 없") ? 400 : 500; + return res.status(status).json({ success: false, message: error.message }); + } +} + +// ─── 반제품 계획 자동 생성 ─── + +export async function generateSemiSchedule(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const createdBy = req.user!.userId; + const { plan_ids, options } = req.body; + + if (!plan_ids || !Array.isArray(plan_ids) || plan_ids.length === 0) { + return res.status(400).json({ success: false, message: "완제품 계획을 선택해주세요" }); + } + + const data = await productionService.generateSemiSchedule( + companyCode, + plan_ids, + options || {}, + createdBy + ); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("반제품 계획 생성 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + +// ─── 스케줄 분할 ─── + +export async function splitSchedule(req: Request, res: Response) { + try { + const companyCode = req.user!.companyCode; + const splitBy = req.user!.userId; + const planId = parseInt(req.params.id, 10); + const { split_qty } = req.body; + + if (!split_qty || split_qty <= 0) { + return res.status(400).json({ success: false, message: "분할 수량을 입력해주세요" }); + } + + const data = await productionService.splitSchedule(companyCode, planId, split_qty, splitBy); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("스케줄 분할 실패", { error: error.message }); + return res.status(error.message.includes("찾을 수 없") ? 404 : 400).json({ + success: false, + message: error.message, + }); + } +} diff --git a/backend-node/src/routes/productionRoutes.ts b/backend-node/src/routes/productionRoutes.ts new file mode 100644 index 00000000..120147f0 --- /dev/null +++ b/backend-node/src/routes/productionRoutes.ts @@ -0,0 +1,36 @@ +/** + * 생산계획 라우트 + */ + +import { Router } from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import * as productionController from "../controllers/productionController"; + +const router = Router(); + +router.use(authenticateToken); + +// 수주 데이터 조회 (품목별 그룹핑) +router.get("/order-summary", productionController.getOrderSummary); + +// 안전재고 부족분 조회 +router.get("/stock-shortage", productionController.getStockShortage); + +// 생산계획 CRUD +router.get("/plan/:id", productionController.getPlanById); +router.put("/plan/:id", productionController.updatePlan); +router.delete("/plan/:id", productionController.deletePlan); + +// 자동 스케줄 생성 +router.post("/generate-schedule", productionController.generateSchedule); + +// 스케줄 병합 +router.post("/merge-schedules", productionController.mergeSchedules); + +// 반제품 계획 자동 생성 +router.post("/generate-semi-schedule", productionController.generateSemiSchedule); + +// 스케줄 분할 +router.post("/plan/:id/split", productionController.splitSchedule); + +export default router; diff --git a/backend-node/src/services/productionPlanService.ts b/backend-node/src/services/productionPlanService.ts new file mode 100644 index 00000000..7c8e69ec --- /dev/null +++ b/backend-node/src/services/productionPlanService.ts @@ -0,0 +1,668 @@ +/** + * 생산계획 서비스 + * - 수주 데이터 조회 (품목별 그룹핑) + * - 안전재고 부족분 조회 + * - 자동 스케줄 생성 + * - 스케줄 병합 + * - 반제품 계획 자동 생성 + * - 스케줄 분할 + */ + +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +// ─── 수주 데이터 조회 (품목별 그룹핑) ─── + +export async function getOrderSummary( + companyCode: string, + options?: { excludePlanned?: boolean; itemCode?: string; itemName?: string } +) { + const pool = getPool(); + const conditions: string[] = ["so.company_code = $1"]; + const params: any[] = [companyCode]; + let paramIdx = 2; + + if (options?.itemCode) { + conditions.push(`so.part_code ILIKE $${paramIdx}`); + params.push(`%${options.itemCode}%`); + paramIdx++; + } + if (options?.itemName) { + conditions.push(`so.part_name ILIKE $${paramIdx}`); + params.push(`%${options.itemName}%`); + paramIdx++; + } + + const whereClause = conditions.join(" AND "); + + const query = ` + WITH order_summary AS ( + SELECT + so.part_code AS item_code, + COALESCE(so.part_name, so.part_code) AS item_name, + SUM(COALESCE(so.order_qty::numeric, 0)) AS total_order_qty, + SUM(COALESCE(so.ship_qty::numeric, 0)) AS total_ship_qty, + SUM(COALESCE(so.balance_qty::numeric, 0)) AS total_balance_qty, + COUNT(*) AS order_count, + MIN(so.due_date) AS earliest_due_date + FROM sales_order_mng so + WHERE ${whereClause} + GROUP BY so.part_code, so.part_name + ), + stock_info AS ( + SELECT + item_code, + SUM(COALESCE(current_qty::numeric, 0)) AS current_stock, + MAX(COALESCE(safety_qty::numeric, 0)) AS safety_stock + FROM inventory_stock + WHERE company_code = $1 + GROUP BY item_code + ), + plan_info AS ( + SELECT + item_code, + SUM(CASE WHEN status = 'planned' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS existing_plan_qty, + SUM(CASE WHEN status = 'in_progress' THEN COALESCE(plan_qty, 0) ELSE 0 END) AS in_progress_qty + FROM production_plan_mng + WHERE company_code = $1 + AND COALESCE(product_type, '완제품') = '완제품' + AND status NOT IN ('completed', 'cancelled') + GROUP BY item_code + ) + SELECT + os.item_code, + os.item_name, + os.total_order_qty, + os.total_ship_qty, + os.total_balance_qty, + os.order_count, + os.earliest_due_date, + COALESCE(si.current_stock, 0) AS current_stock, + COALESCE(si.safety_stock, 0) AS safety_stock, + COALESCE(pi.existing_plan_qty, 0) AS existing_plan_qty, + COALESCE(pi.in_progress_qty, 0) AS in_progress_qty, + GREATEST( + os.total_balance_qty + COALESCE(si.safety_stock, 0) - COALESCE(si.current_stock, 0) + - COALESCE(pi.existing_plan_qty, 0) - COALESCE(pi.in_progress_qty, 0), + 0 + ) AS required_plan_qty + FROM order_summary os + LEFT JOIN stock_info si ON os.item_code = si.item_code + LEFT JOIN plan_info pi ON os.item_code = pi.item_code + ${options?.excludePlanned ? "WHERE COALESCE(pi.existing_plan_qty, 0) = 0" : ""} + ORDER BY os.item_code; + `; + + const result = await pool.query(query, params); + + // 그룹별 상세 수주 데이터도 함께 조회 + const detailWhere = conditions.map(c => c.replace(/so\./g, "")).join(" AND "); + const detailQuery = ` + SELECT + id, order_no, part_code, part_name, + COALESCE(order_qty::numeric, 0) AS order_qty, + COALESCE(ship_qty::numeric, 0) AS ship_qty, + COALESCE(balance_qty::numeric, 0) AS balance_qty, + due_date, status, partner_id, manager_name + FROM sales_order_mng + WHERE ${detailWhere} + ORDER BY part_code, due_date; + `; + const detailResult = await pool.query(detailQuery, params); + + // 그룹별로 상세 데이터 매핑 + const ordersByItem: Record = {}; + for (const row of detailResult.rows) { + const key = row.part_code || "__null__"; + if (!ordersByItem[key]) ordersByItem[key] = []; + ordersByItem[key].push(row); + } + + const data = result.rows.map((group: any) => ({ + ...group, + orders: ordersByItem[group.item_code || "__null__"] || [], + })); + + logger.info("수주 데이터 조회", { companyCode, groupCount: data.length }); + return data; +} + +// ─── 안전재고 부족분 조회 ─── + +export async function getStockShortage(companyCode: string) { + const pool = getPool(); + + const query = ` + SELECT + ist.item_code, + ii.item_name, + COALESCE(ist.current_qty::numeric, 0) AS current_qty, + COALESCE(ist.safety_qty::numeric, 0) AS safety_qty, + (COALESCE(ist.current_qty::numeric, 0) - COALESCE(ist.safety_qty::numeric, 0)) AS shortage_qty, + GREATEST( + COALESCE(ist.safety_qty::numeric, 0) * 2 - COALESCE(ist.current_qty::numeric, 0), 0 + ) AS recommended_qty, + ist.last_in_date + FROM inventory_stock ist + LEFT JOIN item_info ii ON ist.item_code = ii.id AND ist.company_code = ii.company_code + WHERE ist.company_code = $1 + AND COALESCE(ist.current_qty::numeric, 0) < COALESCE(ist.safety_qty::numeric, 0) + ORDER BY shortage_qty ASC; + `; + + const result = await pool.query(query, [companyCode]); + logger.info("안전재고 부족분 조회", { companyCode, count: result.rowCount }); + return result.rows; +} + +// ─── 생산계획 CRUD ─── + +export async function getPlanById(companyCode: string, planId: number) { + const pool = getPool(); + const result = await pool.query( + `SELECT * FROM production_plan_mng WHERE id = $1 AND company_code = $2`, + [planId, companyCode] + ); + return result.rows[0] || null; +} + +export async function updatePlan( + companyCode: string, + planId: number, + data: Record, + updatedBy: string +) { + const pool = getPool(); + + const allowedFields = [ + "plan_qty", "start_date", "end_date", "due_date", + "equipment_id", "equipment_code", "equipment_name", + "manager_name", "work_shift", "priority", "remarks", "status", + "item_code", "item_name", "product_type", "order_no", + ]; + + const setClauses: string[] = []; + const params: any[] = []; + let paramIdx = 1; + + for (const field of allowedFields) { + if (data[field] !== undefined) { + setClauses.push(`${field} = $${paramIdx}`); + params.push(data[field]); + paramIdx++; + } + } + + if (setClauses.length === 0) { + throw new Error("수정할 필드가 없습니다"); + } + + setClauses.push(`updated_date = NOW()`); + setClauses.push(`updated_by = $${paramIdx}`); + params.push(updatedBy); + paramIdx++; + + params.push(planId); + params.push(companyCode); + + const query = ` + UPDATE production_plan_mng + SET ${setClauses.join(", ")} + WHERE id = $${paramIdx - 1} AND company_code = $${paramIdx} + RETURNING * + `; + + const result = await pool.query(query, params); + if (result.rowCount === 0) { + throw new Error("생산계획을 찾을 수 없거나 권한이 없습니다"); + } + logger.info("생산계획 수정", { companyCode, planId }); + return result.rows[0]; +} + +export async function deletePlan(companyCode: string, planId: number) { + const pool = getPool(); + const result = await pool.query( + `DELETE FROM production_plan_mng WHERE id = $1 AND company_code = $2 RETURNING id`, + [planId, companyCode] + ); + if (result.rowCount === 0) { + throw new Error("생산계획을 찾을 수 없거나 권한이 없습니다"); + } + logger.info("생산계획 삭제", { companyCode, planId }); + return { id: planId }; +} + +// ─── 자동 스케줄 생성 ─── + +interface GenerateScheduleItem { + item_code: string; + item_name: string; + required_qty: number; + earliest_due_date: string; + hourly_capacity?: number; + daily_capacity?: number; + lead_time?: number; +} + +interface GenerateScheduleOptions { + safety_lead_time?: number; + recalculate_unstarted?: boolean; + product_type?: string; +} + +export async function generateSchedule( + companyCode: string, + items: GenerateScheduleItem[], + options: GenerateScheduleOptions, + createdBy: string +) { + const pool = getPool(); + const client = await pool.connect(); + const productType = options.product_type || "완제품"; + const safetyLeadTime = options.safety_lead_time || 1; + + try { + await client.query("BEGIN"); + + let deletedCount = 0; + let keptCount = 0; + const newSchedules: any[] = []; + + for (const item of items) { + // 기존 미진행(planned) 스케줄 처리 + if (options.recalculate_unstarted) { + const deleteResult = await client.query( + `DELETE FROM production_plan_mng + WHERE company_code = $1 + AND item_code = $2 + AND COALESCE(product_type, '완제품') = $3 + AND status = 'planned' + RETURNING id`, + [companyCode, item.item_code, productType] + ); + deletedCount += deleteResult.rowCount || 0; + + const keptResult = await client.query( + `SELECT COUNT(*) AS cnt FROM production_plan_mng + WHERE company_code = $1 + AND item_code = $2 + AND COALESCE(product_type, '완제품') = $3 + AND status NOT IN ('planned', 'completed', 'cancelled')`, + [companyCode, item.item_code, productType] + ); + keptCount += parseInt(keptResult.rows[0].cnt, 10); + } + + // 생산일수 계산 + const dailyCapacity = item.daily_capacity || 800; + const requiredQty = item.required_qty; + if (requiredQty <= 0) continue; + + const productionDays = Math.ceil(requiredQty / dailyCapacity); + + // 시작일 = 납기일 - 생산일수 - 안전리드타임 + const dueDate = new Date(item.earliest_due_date); + const endDate = new Date(dueDate); + endDate.setDate(endDate.getDate() - safetyLeadTime); + const startDate = new Date(endDate); + startDate.setDate(startDate.getDate() - productionDays); + + // 시작일이 오늘보다 이전이면 오늘로 조정 + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (startDate < today) { + startDate.setTime(today.getTime()); + endDate.setTime(startDate.getTime()); + endDate.setDate(endDate.getDate() + productionDays); + } + + // 계획번호 생성 + const planNoResult = await client.query( + `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no + FROM production_plan_mng WHERE company_code = $1`, + [companyCode] + ); + const nextNo = planNoResult.rows[0].next_no || 1; + const planNo = `PP-${String(nextNo).padStart(6, "0")}`; + + const insertResult = await client.query( + `INSERT INTO production_plan_mng ( + company_code, plan_no, plan_date, item_code, item_name, + product_type, plan_qty, start_date, end_date, due_date, + status, priority, hourly_capacity, daily_capacity, lead_time, + created_by, created_date, updated_date + ) VALUES ( + $1, $2, CURRENT_DATE, $3, $4, + $5, $6, $7, $8, $9, + 'planned', 'normal', $10, $11, $12, + $13, NOW(), NOW() + ) RETURNING *`, + [ + companyCode, planNo, item.item_code, item.item_name, + productType, requiredQty, + startDate.toISOString().split("T")[0], + endDate.toISOString().split("T")[0], + item.earliest_due_date, + item.hourly_capacity || 100, + dailyCapacity, + item.lead_time || 1, + createdBy, + ] + ); + newSchedules.push(insertResult.rows[0]); + } + + await client.query("COMMIT"); + + const summary = { + total: newSchedules.length + keptCount, + new_count: newSchedules.length, + kept_count: keptCount, + deleted_count: deletedCount, + }; + + logger.info("자동 스케줄 생성 완료", { companyCode, summary }); + return { summary, schedules: newSchedules }; + } catch (error) { + await client.query("ROLLBACK"); + logger.error("자동 스케줄 생성 실패", { companyCode, error }); + throw error; + } finally { + client.release(); + } +} + +// ─── 스케줄 병합 ─── + +export async function mergeSchedules( + companyCode: string, + scheduleIds: number[], + productType: string, + mergedBy: string +) { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // 대상 스케줄 조회 + const placeholders = scheduleIds.map((_, i) => `$${i + 2}`).join(", "); + const targetResult = await client.query( + `SELECT * FROM production_plan_mng + WHERE company_code = $1 AND id IN (${placeholders}) + ORDER BY start_date`, + [companyCode, ...scheduleIds] + ); + + if (targetResult.rowCount !== scheduleIds.length) { + throw new Error("일부 스케줄을 찾을 수 없습니다"); + } + + const rows = targetResult.rows; + + // 동일 품목 검증 + const itemCodes = [...new Set(rows.map((r: any) => r.item_code))]; + if (itemCodes.length > 1) { + throw new Error("동일 품목의 스케줄만 병합할 수 있습니다"); + } + + // 병합 값 계산 + const totalQty = rows.reduce((sum: number, r: any) => sum + (parseFloat(r.plan_qty) || 0), 0); + const earliestStart = rows.reduce( + (min: string, r: any) => (!min || r.start_date < min ? r.start_date : min), + "" + ); + const latestEnd = rows.reduce( + (max: string, r: any) => (!max || r.end_date > max ? r.end_date : max), + "" + ); + const earliestDue = rows.reduce( + (min: string, r: any) => (!min || (r.due_date && r.due_date < min) ? r.due_date : min), + "" + ); + const orderNos = [...new Set(rows.map((r: any) => r.order_no).filter(Boolean))].join(", "); + + // 기존 삭제 + await client.query( + `DELETE FROM production_plan_mng WHERE company_code = $1 AND id IN (${placeholders})`, + [companyCode, ...scheduleIds] + ); + + // 병합된 스케줄 생성 + const planNoResult = await client.query( + `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no + FROM production_plan_mng WHERE company_code = $1`, + [companyCode] + ); + const planNo = `PP-${String(planNoResult.rows[0].next_no || 1).padStart(6, "0")}`; + + const insertResult = await client.query( + `INSERT INTO production_plan_mng ( + company_code, plan_no, plan_date, item_code, item_name, + product_type, plan_qty, start_date, end_date, due_date, + status, order_no, created_by, created_date, updated_date + ) VALUES ( + $1, $2, CURRENT_DATE, $3, $4, + $5, $6, $7, $8, $9, + 'planned', $10, $11, NOW(), NOW() + ) RETURNING *`, + [ + companyCode, planNo, rows[0].item_code, rows[0].item_name, + productType, totalQty, + earliestStart, latestEnd, earliestDue || null, + orderNos || null, mergedBy, + ] + ); + + await client.query("COMMIT"); + logger.info("스케줄 병합 완료", { + companyCode, + mergedFrom: scheduleIds, + mergedTo: insertResult.rows[0].id, + }); + return insertResult.rows[0]; + } catch (error) { + await client.query("ROLLBACK"); + logger.error("스케줄 병합 실패", { companyCode, error }); + throw error; + } finally { + client.release(); + } +} + +// ─── 반제품 계획 자동 생성 ─── + +export async function generateSemiSchedule( + companyCode: string, + planIds: number[], + options: { considerStock?: boolean; excludeUsed?: boolean }, + createdBy: string +) { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // 선택된 완제품 계획 조회 + const placeholders = planIds.map((_, i) => `$${i + 2}`).join(", "); + const plansResult = await client.query( + `SELECT * FROM production_plan_mng + WHERE company_code = $1 AND id IN (${placeholders})`, + [companyCode, ...planIds] + ); + + const newSemiPlans: any[] = []; + + for (const plan of plansResult.rows) { + // BOM에서 해당 품목의 반제품 소요량 조회 + const bomQuery = ` + SELECT + bd.child_item_id, + ii.item_name AS child_item_name, + ii.item_code AS child_item_code, + bd.quantity AS bom_qty, + bd.unit + FROM bom b + JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code + LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code + WHERE b.company_code = $1 + AND b.item_code = $2 + AND COALESCE(b.status, 'active') = 'active' + `; + const bomResult = await client.query(bomQuery, [companyCode, plan.item_code]); + + for (const bomItem of bomResult.rows) { + let requiredQty = (parseFloat(plan.plan_qty) || 0) * (parseFloat(bomItem.bom_qty) || 1); + + // 재고 고려 + if (options.considerStock) { + const stockResult = await client.query( + `SELECT COALESCE(SUM(current_qty::numeric), 0) AS stock + FROM inventory_stock + WHERE company_code = $1 AND item_code = $2`, + [companyCode, bomItem.child_item_code || bomItem.child_item_id] + ); + const stock = parseFloat(stockResult.rows[0].stock) || 0; + requiredQty = Math.max(requiredQty - stock, 0); + } + + if (requiredQty <= 0) continue; + + // 반제품 납기일 = 완제품 시작일 + const semiDueDate = plan.start_date; + const semiEndDate = plan.start_date; + const semiStartDate = new Date(plan.start_date); + semiStartDate.setDate(semiStartDate.getDate() - (plan.lead_time || 1)); + + const planNoResult = await client.query( + `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no + FROM production_plan_mng WHERE company_code = $1`, + [companyCode] + ); + const planNo = `PP-${String(planNoResult.rows[0].next_no || 1).padStart(6, "0")}`; + + const insertResult = await client.query( + `INSERT INTO production_plan_mng ( + company_code, plan_no, plan_date, item_code, item_name, + product_type, plan_qty, start_date, end_date, due_date, + status, parent_plan_id, created_by, created_date, updated_date + ) VALUES ( + $1, $2, CURRENT_DATE, $3, $4, + '반제품', $5, $6, $7, $8, + 'planned', $9, $10, NOW(), NOW() + ) RETURNING *`, + [ + companyCode, planNo, + bomItem.child_item_code || bomItem.child_item_id, + bomItem.child_item_name || bomItem.child_item_id, + requiredQty, + semiStartDate.toISOString().split("T")[0], + typeof semiEndDate === "string" ? semiEndDate : semiEndDate.toISOString().split("T")[0], + typeof semiDueDate === "string" ? semiDueDate : semiDueDate.toISOString().split("T")[0], + plan.id, + createdBy, + ] + ); + newSemiPlans.push(insertResult.rows[0]); + } + } + + await client.query("COMMIT"); + logger.info("반제품 계획 생성 완료", { + companyCode, + parentPlanIds: planIds, + semiPlanCount: newSemiPlans.length, + }); + return { count: newSemiPlans.length, schedules: newSemiPlans }; + } catch (error) { + await client.query("ROLLBACK"); + logger.error("반제품 계획 생성 실패", { companyCode, error }); + throw error; + } finally { + client.release(); + } +} + +// ─── 스케줄 분할 ─── + +export async function splitSchedule( + companyCode: string, + planId: number, + splitQty: number, + splitBy: string +) { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + const planResult = await client.query( + `SELECT * FROM production_plan_mng WHERE id = $1 AND company_code = $2`, + [planId, companyCode] + ); + if (planResult.rowCount === 0) { + throw new Error("생산계획을 찾을 수 없습니다"); + } + + const plan = planResult.rows[0]; + const originalQty = parseFloat(plan.plan_qty) || 0; + + if (splitQty >= originalQty || splitQty <= 0) { + throw new Error("분할 수량은 0보다 크고 원래 수량보다 작아야 합니다"); + } + + // 원본 수량 감소 + await client.query( + `UPDATE production_plan_mng SET plan_qty = $1, updated_date = NOW(), updated_by = $2 + WHERE id = $3 AND company_code = $4`, + [originalQty - splitQty, splitBy, planId, companyCode] + ); + + // 분할된 새 계획 생성 + const planNoResult = await client.query( + `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no + FROM production_plan_mng WHERE company_code = $1`, + [companyCode] + ); + const planNo = `PP-${String(planNoResult.rows[0].next_no || 1).padStart(6, "0")}`; + + const insertResult = await client.query( + `INSERT INTO production_plan_mng ( + company_code, plan_no, plan_date, item_code, item_name, + product_type, plan_qty, start_date, end_date, due_date, + status, priority, equipment_id, equipment_code, equipment_name, + order_no, parent_plan_id, created_by, created_date, updated_date + ) VALUES ( + $1, $2, CURRENT_DATE, $3, $4, + $5, $6, $7, $8, $9, + $10, $11, $12, $13, $14, + $15, $16, $17, NOW(), NOW() + ) RETURNING *`, + [ + companyCode, planNo, plan.item_code, plan.item_name, + plan.product_type, splitQty, + plan.start_date, plan.end_date, plan.due_date, + plan.status, plan.priority, plan.equipment_id, plan.equipment_code, plan.equipment_name, + plan.order_no, plan.parent_plan_id, + splitBy, + ] + ); + + await client.query("COMMIT"); + logger.info("스케줄 분할 완료", { companyCode, planId, splitQty }); + return { + original: { id: planId, plan_qty: originalQty - splitQty }, + split: insertResult.rows[0], + }; + } catch (error) { + await client.query("ROLLBACK"); + logger.error("스케줄 분할 실패", { companyCode, error }); + throw error; + } finally { + client.release(); + } +} diff --git a/frontend/lib/api/production.ts b/frontend/lib/api/production.ts new file mode 100644 index 00000000..244ef426 --- /dev/null +++ b/frontend/lib/api/production.ts @@ -0,0 +1,178 @@ +/** + * 생산계획 API 클라이언트 + */ + +import apiClient from "./client"; + +// ─── 타입 정의 ─── + +export interface OrderSummaryItem { + item_code: string; + item_name: string; + total_order_qty: number; + total_ship_qty: number; + total_balance_qty: number; + order_count: number; + earliest_due_date: string | null; + current_stock: number; + safety_stock: number; + existing_plan_qty: number; + in_progress_qty: number; + required_plan_qty: number; + orders: OrderDetail[]; +} + +export interface OrderDetail { + id: string; + order_no: string; + part_code: string; + part_name: string; + order_qty: number; + ship_qty: number; + balance_qty: number; + due_date: string | null; + status: string; + customer_name: string | null; +} + +export interface StockShortageItem { + item_code: string; + item_name: string; + current_qty: number; + safety_qty: number; + shortage_qty: number; + recommended_qty: number; + last_in_date: string | null; +} + +export interface ProductionPlan { + id: number; + company_code: string; + plan_no: string; + plan_date: string; + item_code: string; + item_name: string; + product_type: string; + plan_qty: number; + completed_qty: number; + progress_rate: number; + start_date: string; + end_date: string; + due_date: string | null; + equipment_id: number | null; + equipment_code: string | null; + equipment_name: string | null; + status: string; + priority: string | null; + order_no: string | null; + parent_plan_id: number | null; + remarks: string | null; +} + +export interface GenerateScheduleRequest { + items: { + item_code: string; + item_name: string; + required_qty: number; + earliest_due_date: string; + hourly_capacity?: number; + daily_capacity?: number; + lead_time?: number; + }[]; + options?: { + safety_lead_time?: number; + recalculate_unstarted?: boolean; + product_type?: string; + }; +} + +export interface GenerateScheduleResponse { + summary: { + total: number; + new_count: number; + kept_count: number; + deleted_count: number; + }; + schedules: ProductionPlan[]; +} + +// ─── API 함수 ─── + +/** 수주 데이터 조회 (품목별 그룹핑) */ +export async function getOrderSummary(params?: { + excludePlanned?: boolean; + itemCode?: string; + itemName?: string; +}) { + const queryParams = new URLSearchParams(); + if (params?.excludePlanned) queryParams.set("excludePlanned", "true"); + if (params?.itemCode) queryParams.set("itemCode", params.itemCode); + if (params?.itemName) queryParams.set("itemName", params.itemName); + + const qs = queryParams.toString(); + const url = `/api/production/order-summary${qs ? `?${qs}` : ""}`; + const response = await apiClient.get(url); + return response.data as { success: boolean; data: OrderSummaryItem[] }; +} + +/** 안전재고 부족분 조회 */ +export async function getStockShortage() { + const response = await apiClient.get("/api/production/stock-shortage"); + return response.data as { success: boolean; data: StockShortageItem[] }; +} + +/** 생산계획 상세 조회 */ +export async function getPlanById(planId: number) { + const response = await apiClient.get(`/api/production/plan/${planId}`); + return response.data as { success: boolean; data: ProductionPlan }; +} + +/** 생산계획 수정 */ +export async function updatePlan(planId: number, data: Partial) { + const response = await apiClient.put(`/api/production/plan/${planId}`, data); + return response.data as { success: boolean; data: ProductionPlan }; +} + +/** 생산계획 삭제 */ +export async function deletePlan(planId: number) { + const response = await apiClient.delete(`/api/production/plan/${planId}`); + return response.data as { success: boolean; message: string }; +} + +/** 자동 스케줄 생성 */ +export async function generateSchedule(request: GenerateScheduleRequest) { + const response = await apiClient.post("/api/production/generate-schedule", request); + return response.data as { success: boolean; data: GenerateScheduleResponse }; +} + +/** 스케줄 병합 */ +export async function mergeSchedules(scheduleIds: number[], productType?: string) { + const response = await apiClient.post("/api/production/merge-schedules", { + schedule_ids: scheduleIds, + product_type: productType || "완제품", + }); + return response.data as { success: boolean; data: ProductionPlan }; +} + +/** 반제품 계획 자동 생성 */ +export async function generateSemiSchedule( + planIds: number[], + options?: { considerStock?: boolean; excludeUsed?: boolean } +) { + const response = await apiClient.post("/api/production/generate-semi-schedule", { + plan_ids: planIds, + options: options || {}, + }); + return response.data as { success: boolean; data: { count: number; schedules: ProductionPlan[] } }; +} + +/** 스케줄 분할 */ +export async function splitSchedule(planId: number, splitQty: number) { + const response = await apiClient.post(`/api/production/plan/${planId}/split`, { + split_qty: splitQty, + }); + return response.data as { + success: boolean; + data: { original: { id: number; plan_qty: number }; split: ProductionPlan }; + }; +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts index 94c001d4..8e2e1b53 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/hooks/useTimelineData.ts @@ -124,10 +124,16 @@ export function useTimelineData( sourceKeys: currentSourceKeys, }); + const searchParams: Record = {}; + if (!isScheduleMng && config.staticFilters) { + Object.assign(searchParams, config.staticFilters); + } + const response = await apiClient.post(`/table-management/tables/${tableName}/data`, { page: 1, size: 10000, autoFilter: true, + ...(Object.keys(searchParams).length > 0 ? { search: searchParams } : {}), }); const responseData = response.data?.data?.data || response.data?.data || []; @@ -195,7 +201,8 @@ export function useTimelineData( setIsLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tableName, externalSchedules, fieldMappingKey, config.scheduleType]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableName, externalSchedules, fieldMappingKey, config.scheduleType, JSON.stringify(config.staticFilters)]); // 리소스 데이터 로드 const fetchResources = useCallback(async () => { diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts index baf59741..afcc9f5e 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -144,6 +144,9 @@ export interface TimelineSchedulerConfig extends ComponentConfig { /** 커스텀 테이블명 */ customTableName?: string; + /** 정적 필터 조건 (커스텀 테이블에서 특정 조건으로 필터링) */ + staticFilters?: Record; + /** 리소스 테이블명 (설비/작업자) */ resourceTable?: string; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index cc145262..1d8a3197 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -59,7 +59,8 @@ export type ButtonActionType = | "transferData" // 데이터 전달 (컴포넌트 간 or 화면 간) | "quickInsert" // 즉시 저장 (선택한 데이터를 특정 테이블에 즉시 INSERT) | "event" // 이벤트 버스로 이벤트 발송 (스케줄 생성 등) - | "approval"; // 결재 요청 + | "approval" // 결재 요청 + | "apiCall"; // 범용 API 호출 (생산계획 자동 스케줄 등) /** * 버튼 액션 설정 @@ -286,6 +287,18 @@ export interface ButtonActionConfig { eventName: string; // 발송할 이벤트 이름 (V2_EVENTS 키) eventPayload?: Record; // 이벤트 페이로드 (requestId는 자동 생성) }; + + // 범용 API 호출 관련 (apiCall 액션용) + apiCallConfig?: { + method: "GET" | "POST" | "PUT" | "DELETE"; + endpoint: string; // 예: "/api/production/generate-schedule" + payloadMapping?: Record; // formData 필드 → API body 필드 매핑 + staticPayload?: Record; // 고정 페이로드 값 + useSelectedRows?: boolean; // true면 선택된 행 데이터를 body에 포함 + selectedRowsKey?: string; // 선택된 행 데이터의 key (기본: "items") + refreshAfterSuccess?: boolean; // 성공 후 테이블 새로고침 (기본: true) + confirmMessage?: string; // 실행 전 확인 메시지 + }; } /** @@ -457,6 +470,9 @@ export class ButtonActionExecutor { case "event": return await this.handleEvent(config, context); + case "apiCall": + return await this.handleApiCall(config, context); + case "approval": return this.handleApproval(config, context); @@ -7681,6 +7697,97 @@ export class ButtonActionExecutor { } } + /** + * 범용 API 호출 (생산계획 자동 스케줄 등) + */ + private static async handleApiCall(config: ButtonActionConfig, context: ButtonActionContext): Promise { + try { + const { apiCallConfig } = config; + + if (!apiCallConfig?.endpoint) { + toast.error("API 엔드포인트가 설정되지 않았습니다."); + return false; + } + + // 확인 메시지 + if (apiCallConfig.confirmMessage) { + const confirmed = window.confirm(apiCallConfig.confirmMessage); + if (!confirmed) return false; + } + + // 페이로드 구성 + let payload: Record = { ...(apiCallConfig.staticPayload || {}) }; + + // formData에서 매핑 + if (apiCallConfig.payloadMapping && context.formData) { + for (const [formField, apiField] of Object.entries(apiCallConfig.payloadMapping)) { + if (context.formData[formField] !== undefined) { + payload[apiField] = context.formData[formField]; + } + } + } + + // 선택된 행 데이터 포함 + if (apiCallConfig.useSelectedRows && context.selectedRowsData) { + const key = apiCallConfig.selectedRowsKey || "items"; + payload[key] = context.selectedRowsData; + } + + console.log("[handleApiCall] API 호출:", { + method: apiCallConfig.method, + endpoint: apiCallConfig.endpoint, + payload, + }); + + // API 호출 + const { apiClient } = await import("@/lib/api/client"); + let response: any; + + switch (apiCallConfig.method) { + case "GET": + response = await apiClient.get(apiCallConfig.endpoint, { params: payload }); + break; + case "POST": + response = await apiClient.post(apiCallConfig.endpoint, payload); + break; + case "PUT": + response = await apiClient.put(apiCallConfig.endpoint, payload); + break; + case "DELETE": + response = await apiClient.delete(apiCallConfig.endpoint, { data: payload }); + break; + } + + const result = response?.data; + + if (result?.success === false) { + toast.error(result.message || "API 호출에 실패했습니다."); + return false; + } + + // 성공 메시지 + if (config.successMessage) { + toast.success(config.successMessage); + } + + // 테이블 새로고침 + if (apiCallConfig.refreshAfterSuccess !== false) { + const { v2EventBus, V2_EVENTS } = await import("@/lib/v2-core"); + v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, { + tableName: context.tableName, + target: "all", + }); + } + + return true; + } catch (error: any) { + console.error("[handleApiCall] API 호출 오류:", error); + const msg = error?.response?.data?.message || error?.message || "API 호출 중 오류가 발생했습니다."; + toast.error(msg); + return false; + } + } + /** * 결재 요청 모달 열기 */ @@ -7843,4 +7950,8 @@ export const DEFAULT_BUTTON_ACTIONS: Record Date: Mon, 16 Mar 2026 09:35:23 +0900 Subject: [PATCH 12/18] refactor: update production controller to use AuthenticatedRequest - Modified the production controller to replace the generic Request type with AuthenticatedRequest for better type safety and to ensure user authentication is handled correctly. - This change enhances the security and clarity of the API endpoints related to production plan management, ensuring that user-specific data is accessed appropriately. Made-with: Cursor --- .../src/controllers/productionController.ts | 21 +++--- .../TimelineSchedulerComponent.tsx | 64 +++++++++---------- .../components/ResourceRow.tsx | 8 +-- .../components/ScheduleBar.tsx | 16 ++--- .../components/TimelineHeader.tsx | 10 +-- 5 files changed, 60 insertions(+), 59 deletions(-) diff --git a/backend-node/src/controllers/productionController.ts b/backend-node/src/controllers/productionController.ts index 9d6c56c4..aa3f3a36 100644 --- a/backend-node/src/controllers/productionController.ts +++ b/backend-node/src/controllers/productionController.ts @@ -2,13 +2,14 @@ * 생산계획 컨트롤러 */ -import { Request, Response } from "express"; +import { Response } from "express"; +import { AuthenticatedRequest } from "../types/auth"; import * as productionService from "../services/productionPlanService"; import { logger } from "../utils/logger"; // ─── 수주 데이터 조회 (품목별 그룹핑) ─── -export async function getOrderSummary(req: Request, res: Response) { +export async function getOrderSummary(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const { excludePlanned, itemCode, itemName } = req.query; @@ -28,7 +29,7 @@ export async function getOrderSummary(req: Request, res: Response) { // ─── 안전재고 부족분 조회 ─── -export async function getStockShortage(req: Request, res: Response) { +export async function getStockShortage(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const data = await productionService.getStockShortage(companyCode); @@ -41,7 +42,7 @@ export async function getStockShortage(req: Request, res: Response) { // ─── 생산계획 상세 조회 ─── -export async function getPlanById(req: Request, res: Response) { +export async function getPlanById(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const planId = parseInt(req.params.id, 10); @@ -59,7 +60,7 @@ export async function getPlanById(req: Request, res: Response) { // ─── 생산계획 수정 ─── -export async function updatePlan(req: Request, res: Response) { +export async function updatePlan(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const planId = parseInt(req.params.id, 10); @@ -78,7 +79,7 @@ export async function updatePlan(req: Request, res: Response) { // ─── 생산계획 삭제 ─── -export async function deletePlan(req: Request, res: Response) { +export async function deletePlan(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const planId = parseInt(req.params.id, 10); @@ -96,7 +97,7 @@ export async function deletePlan(req: Request, res: Response) { // ─── 자동 스케줄 생성 ─── -export async function generateSchedule(req: Request, res: Response) { +export async function generateSchedule(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const createdBy = req.user!.userId; @@ -116,7 +117,7 @@ export async function generateSchedule(req: Request, res: Response) { // ─── 스케줄 병합 ─── -export async function mergeSchedules(req: Request, res: Response) { +export async function mergeSchedules(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const mergedBy = req.user!.userId; @@ -142,7 +143,7 @@ export async function mergeSchedules(req: Request, res: Response) { // ─── 반제품 계획 자동 생성 ─── -export async function generateSemiSchedule(req: Request, res: Response) { +export async function generateSemiSchedule(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const createdBy = req.user!.userId; @@ -167,7 +168,7 @@ export async function generateSemiSchedule(req: Request, res: Response) { // ─── 스케줄 분할 ─── -export async function splitSchedule(req: Request, res: Response) { +export async function splitSchedule(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; const splitBy = req.user!.userId; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx index f6fbaea2..e7da45a6 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx @@ -226,11 +226,11 @@ export function TimelineSchedulerComponent({ // 디자인 모드 플레이스홀더 if (isDesignMode) { return ( -
+
- -

타임라인 스케줄러

-

+ +

타임라인 스케줄러

+

{config.selectedTable ? `테이블: ${config.selectedTable}` : "테이블을 선택하세요"} @@ -244,12 +244,12 @@ export function TimelineSchedulerComponent({ if (isLoading) { return (

- - 로딩 중... + + 로딩 중...
); @@ -259,12 +259,12 @@ export function TimelineSchedulerComponent({ if (error) { return (
-

오류 발생

-

{error}

+

오류 발생

+

{error}

); @@ -274,13 +274,13 @@ export function TimelineSchedulerComponent({ if (schedules.length === 0) { return (
- -

스케줄 데이터가 없습니다

-

+ +

스케줄 데이터가 없습니다

+

좌측 테이블에서 품목을 선택하거나,
스케줄 생성 버튼을 눌러 스케줄을 생성하세요

@@ -292,7 +292,7 @@ export function TimelineSchedulerComponent({ return (
{/* 툴바 */} {config.showToolbar !== false && ( -
+
{/* 네비게이션 */} -
+
{config.showNavigation !== false && ( <> @@ -325,15 +325,15 @@ export function TimelineSchedulerComponent({ variant="ghost" size="sm" onClick={goToNext} - className="h-7 px-2" + className="h-6 px-1.5 sm:h-7 sm:px-2" > - + )} {/* 현재 날짜 범위 표시 */} - + {viewStartDate.getFullYear()}년 {viewStartDate.getMonth() + 1}월{" "} {viewStartDate.getDate()}일 ~{" "} {viewEndDate.getMonth() + 1}월 {viewEndDate.getDate()}일 @@ -341,20 +341,20 @@ export function TimelineSchedulerComponent({
{/* 오른쪽 컨트롤 */} -
+
{/* 줌 컨트롤 */} {config.showZoomControls !== false && ( -
+
- + {zoomLevelOptions.find((o) => o.value === zoomLevel)?.label}
)} @@ -375,9 +375,9 @@ export function TimelineSchedulerComponent({ variant="default" size="sm" onClick={handleAddClick} - className="h-7" + className="h-6 text-xs sm:h-7 sm:text-sm" > - + 추가 )} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx index 407bdd14..75a465a3 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx @@ -138,13 +138,13 @@ export function ResourceRow({ > {/* 리소스 컬럼 */}
-
{resource.name}
+
{resource.name}
{resource.group && ( -
+
{resource.group}
)} @@ -170,7 +170,7 @@ export function ResourceRow({
)} {/* 제목 */} -
+
{schedule.title}
{/* 진행률 텍스트 */} {config.showProgress && schedule.progress !== undefined && ( -
+
{schedule.progress}%
)} @@ -165,7 +165,7 @@ export function ScheduleBar({ {/* 리사이즈 핸들 - 왼쪽 */} {resizable && (
handleResizeStart("start", e)} /> )} @@ -173,7 +173,7 @@ export function ScheduleBar({ {/* 리사이즈 핸들 - 오른쪽 */} {resizable && (
handleResizeStart("end", e)} /> )} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx index 52afc2e2..b3f89cfc 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineHeader.tsx @@ -140,7 +140,7 @@ export function TimelineHeader({
{/* 리소스 컬럼 헤더 */}
리소스 @@ -150,7 +150,7 @@ export function TimelineHeader({ {monthGroups.map((group, idx) => (
{group.year}년 {group.month} @@ -162,7 +162,7 @@ export function TimelineHeader({
{/* 리소스 컬럼 (빈칸) */}
@@ -171,7 +171,7 @@ export function TimelineHeader({
)} From 3bd0eff82e3d577c1b1c675dde48d188c0a6b732 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 16 Mar 2026 10:32:58 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20BLOCK=20DETAIL=20Phase=203=20-=20?= =?UTF-8?q?pop-work-detail=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20+=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=84=B8=EB=B6=80=EC=A7=84=ED=96=89?= =?UTF-8?q?=ED=99=94=EB=A9=B4(4502)=EC=9D=98=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EA=B5=AC=ED=98=84:=20pop-work-detail=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=8B=A0=EA=B7=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B3=BC=20=EB=94=94=EC=9E=90=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20=EB=AA=A8=EB=8B=AC=20=EC=BA=94=EB=B2=84=EC=8A=A4=20?= =?UTF-8?q?=ED=8E=B8=EC=A7=91=EC=9D=84=20=ED=86=B5=ED=95=B4,=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EA=B3=B5=EC=A0=95?= =?UTF-8?q?=EB=B3=84=20=EC=B2=B4=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8/?= =?UTF-8?q?=EA=B2=80=EC=82=AC/=EC=8B=A4=EC=A0=81=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EB=AA=A8=EB=8B=AC=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= =?UTF-8?q?.=20[=EC=8B=A0=EA=B7=9C]=20pop-work-detail=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20(4=ED=8C=8C=EC=9D=BC)=20-=20PopWorkDetailC?= =?UTF-8?q?omponent:=20parentRow=20=E2=86=92=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EA=B3=B5=EC=A0=95=20=EC=B6=94=EC=B6=9C=20=E2=86=92=20process?= =?UTF-8?q?=5Fwork=5Fresult=20=EC=A1=B0=ED=9A=8C,=20=20=20=EC=A2=8C?= =?UTF-8?q?=EC=B8=A1=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94(PRE/IN/POST=20?= =?UTF-8?q?3=EB=8B=A8=EA=B3=84=20=EC=9E=91=EC=97=85=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9)=20+=20=EC=9A=B0=EC=B8=A1=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8(5=EC=A2=85:=20check/inspec?= =?UTF-8?q?t/=20=20=20input/procedure/material)=20+=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=A8=B8=20=EC=A0=9C=EC=96=B4(start/pause/resume)=20+=20?= =?UTF-8?q?=EC=88=98=EB=9F=89=20=EB=93=B1=EB=A1=9D=20+=20=EA=B3=B5?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C=20-=20PopWorkDetailConfig:=20sho?= =?UTF-8?q?wTimer/showQuantityInput/phaseLabels=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20-=20PopWorkDetailPreview:=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B4=EB=84=88=20=ED=94=84=EB=A6=AC=EB=B7=B0=20-?= =?UTF-8?q?=20index.tsx:=20PopComponentRegistry=20=EB=93=B1=EB=A1=9D=20(ca?= =?UTF-8?q?tegory:=20display,=20touchOptimized)=20[=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EC=BA=94=EB=B2=84=EC=8A=A4=20=EC=8B=9C=EC=8A=A4=ED=85=9C]=20Po?= =?UTF-8?q?pDesigner.tsx=20=EB=8C=80=EA=B7=9C=EB=AA=A8=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20handleMoveComponent/handleRes?= =?UTF-8?q?izeComponent/handleRequestResize:=20=20=20layout=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EC=B0=B8=EC=A1=B0=20=E2=86=92=20setLayout(prev=20?= =?UTF-8?q?=3D>=20...)=20=ED=95=A8=EC=88=98=ED=98=95=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=A1=9C=20=EC=A0=84=ED=99=98=20=20=20+=20ac?= =?UTF-8?q?tiveCanvasId=20=EB=B6=84=EA=B8=B0:=20main=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EB=A1=9C=EC=A7=81,=20modal-*=EC=9D=B4?= =?UTF-8?q?=EB=A9=B4=20modals=20=EB=B0=B0=EC=97=B4=20=EB=82=B4=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EB=AA=A8=EB=8B=AC=20=EC=88=98=EC=A0=95=20-=20PopCa?= =?UTF-8?q?rdListV2Config:=20=EB=AA=A8=EB=8B=AC=20=EC=BA=94=EB=B2=84?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1/=EC=97=B4=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20(usePopDesignerContext=20=EC=97=B0=EB=8F=99)=20-=20?= =?UTF-8?q?PopCardListV2Component:=20modal-*=20screenId=20=E2=86=92=20setS?= =?UTF-8?q?haredData=20+=20=5F=5Fpop=5Fmodal=5Fopen=5F=5F=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20-=20PopViewerWithModals:=20parentRow=20pro?= =?UTF-8?q?p=20+=20fullscreen=20=EB=AA=A8=EB=8B=AC=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?+=20flex=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20[=EA=B8=B0?= =?UTF-8?q?=ED=83=80]=20-=20ComponentPalette:=20pop-work-detail=20?= =?UTF-8?q?=ED=8C=94=EB=A0=88=ED=8A=B8=20=ED=95=AD=EB=AA=A9=20+=20Clipboar?= =?UTF-8?q?dCheck=20=EC=95=84=EC=9D=B4=EC=BD=98=20-=20pop-layout.ts:=20Pop?= =?UTF-8?q?ComponentType=EC=97=90=20pop-work-detail=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=ED=81=AC=EA=B8=B0=2038x26=20-=20PopRen?= =?UTF-8?q?derer:=20COMPONENT=5FTYPE=5FLABELS=EC=97=90=20pop-work-detail?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20types.ts:=20PopWorkDetailConfig=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20-=20PopCanvas.t?= =?UTF-8?q?sx:=20activeLayout.components=20=EC=B0=B8=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(=EB=AA=A8=EB=8B=AC=20=EC=BA=94=EB=B2=84=EC=8A=A4?= =?UTF-8?q?=20=ED=98=B8=ED=99=98)=20DB=20=EB=B3=80=EA=B2=BD=200=EA=B1=B4.?= =?UTF-8?q?=20=EB=B0=B1=EC=97=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=200?= =?UTF-8?q?=EA=B1=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pop/designer/PopCanvas.tsx | 2 +- .../components/pop/designer/PopDesigner.tsx | 345 +++++--- .../pop/designer/panels/ComponentPalette.tsx | 8 +- .../pop/designer/renderers/PopRenderer.tsx | 1 + .../pop/designer/types/pop-layout.ts | 3 +- .../pop/viewer/PopViewerWithModals.tsx | 34 +- frontend/lib/registry/pop-components/index.ts | 1 + .../PopCardListV2Component.tsx | 12 +- .../pop-card-list-v2/PopCardListV2Config.tsx | 57 +- .../PopWorkDetailComponent.tsx | 832 ++++++++++++++++++ .../pop-work-detail/PopWorkDetailConfig.tsx | 72 ++ .../pop-work-detail/PopWorkDetailPreview.tsx | 30 + .../pop-components/pop-work-detail/index.tsx | 39 + frontend/lib/registry/pop-components/types.ts | 11 + 14 files changed, 1299 insertions(+), 148 deletions(-) create mode 100644 frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailComponent.tsx create mode 100644 frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx create mode 100644 frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailPreview.tsx create mode 100644 frontend/lib/registry/pop-components/pop-work-detail/index.tsx diff --git a/frontend/components/pop/designer/PopCanvas.tsx b/frontend/components/pop/designer/PopCanvas.tsx index d12422ec..a306c1f1 100644 --- a/frontend/components/pop/designer/PopCanvas.tsx +++ b/frontend/components/pop/designer/PopCanvas.tsx @@ -403,7 +403,7 @@ export default function PopCanvas({ // 이동 중인 컴포넌트의 현재 유효 위치에서 colSpan/rowSpan 가져오기 // 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용 const currentEffectivePos = effectivePositions.get(dragItem.componentId); - const componentData = layout.components[dragItem.componentId]; + const componentData = activeLayout.components[dragItem.componentId]; if (!currentEffectivePos && !componentData) return; diff --git a/frontend/components/pop/designer/PopDesigner.tsx b/frontend/components/pop/designer/PopDesigner.tsx index 259ead41..8e6df1a3 100644 --- a/frontend/components/pop/designer/PopDesigner.tsx +++ b/frontend/components/pop/designer/PopDesigner.tsx @@ -389,97 +389,156 @@ export default function PopDesigner({ const handleMoveComponent = useCallback( (componentId: string, newPosition: PopGridPosition) => { - const component = layout.components[componentId]; - if (!component) return; - - // 기본 모드(tablet_landscape, 12칸)인 경우: 원본 position 직접 수정 - if (currentMode === "tablet_landscape") { - const newLayout = { - ...layout, - components: { - ...layout.components, - [componentId]: { - ...component, - position: newPosition, - }, - }, - }; - setLayout(newLayout); - saveToHistory(newLayout); - setHasChanges(true); - } else { - // 다른 모드인 경우: 오버라이드에 저장 - // 숨김 상태였던 컴포넌트를 이동하면 숨김 해제도 함께 처리 - const currentHidden = layout.overrides?.[currentMode]?.hidden || []; - const isHidden = currentHidden.includes(componentId); - const newHidden = isHidden - ? currentHidden.filter(id => id !== componentId) - : currentHidden; - - const newLayout = { - ...layout, - overrides: { - ...layout.overrides, - [currentMode]: { - ...layout.overrides?.[currentMode], - positions: { - ...layout.overrides?.[currentMode]?.positions, - [componentId]: newPosition, + setLayout((prev) => { + if (activeCanvasId === "main") { + const component = prev.components[componentId]; + if (!component) return prev; + + if (currentMode === "tablet_landscape") { + const newLayout = { + ...prev, + components: { + ...prev.components, + [componentId]: { ...component, position: newPosition }, }, - // 숨김 배열 업데이트 (빈 배열이면 undefined로) - hidden: newHidden.length > 0 ? newHidden : undefined, - }, - }, - }; - setLayout(newLayout); - saveToHistory(newLayout); - setHasChanges(true); - } + }; + saveToHistory(newLayout); + return newLayout; + } else { + const currentHidden = prev.overrides?.[currentMode]?.hidden || []; + const newHidden = currentHidden.filter(id => id !== componentId); + const newLayout = { + ...prev, + overrides: { + ...prev.overrides, + [currentMode]: { + ...prev.overrides?.[currentMode], + positions: { + ...prev.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + hidden: newHidden.length > 0 ? newHidden : undefined, + }, + }, + }; + saveToHistory(newLayout); + return newLayout; + } + } else { + // 모달 캔버스 + const newLayout = { + ...prev, + modals: (prev.modals || []).map(m => { + if (m.id !== activeCanvasId) return m; + const component = m.components[componentId]; + if (!component) return m; + + if (currentMode === "tablet_landscape") { + return { + ...m, + components: { + ...m.components, + [componentId]: { ...component, position: newPosition }, + }, + }; + } else { + const currentHidden = m.overrides?.[currentMode]?.hidden || []; + const newHidden = currentHidden.filter(id => id !== componentId); + return { + ...m, + overrides: { + ...m.overrides, + [currentMode]: { + ...m.overrides?.[currentMode], + positions: { + ...m.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + hidden: newHidden.length > 0 ? newHidden : undefined, + }, + }, + }; + } + }), + }; + saveToHistory(newLayout); + return newLayout; + } + }); + setHasChanges(true); }, - [layout, saveToHistory, currentMode] + [saveToHistory, currentMode, activeCanvasId] ); const handleResizeComponent = useCallback( (componentId: string, newPosition: PopGridPosition) => { - const component = layout.components[componentId]; - if (!component) return; - - // 기본 모드(tablet_landscape, 12칸)인 경우: 원본 position 직접 수정 - if (currentMode === "tablet_landscape") { - const newLayout = { - ...layout, - components: { - ...layout.components, - [componentId]: { - ...component, - position: newPosition, - }, - }, - }; - setLayout(newLayout); - // 리사이즈는 드래그 중 계속 호출되므로 히스토리는 마우스업 시에만 저장 - // 현재는 간단히 매번 저장 (최적화 가능) - setHasChanges(true); - } else { - // 다른 모드인 경우: 오버라이드에 저장 - const newLayout = { - ...layout, - overrides: { - ...layout.overrides, - [currentMode]: { - ...layout.overrides?.[currentMode], - positions: { - ...layout.overrides?.[currentMode]?.positions, - [componentId]: newPosition, + setLayout((prev) => { + if (activeCanvasId === "main") { + const component = prev.components[componentId]; + if (!component) return prev; + + if (currentMode === "tablet_landscape") { + return { + ...prev, + components: { + ...prev.components, + [componentId]: { ...component, position: newPosition }, }, - }, - }, - }; - setLayout(newLayout); - setHasChanges(true); - } + }; + } else { + return { + ...prev, + overrides: { + ...prev.overrides, + [currentMode]: { + ...prev.overrides?.[currentMode], + positions: { + ...prev.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + }, + }, + }; + } + } else { + // 모달 캔버스 + return { + ...prev, + modals: (prev.modals || []).map(m => { + if (m.id !== activeCanvasId) return m; + const component = m.components[componentId]; + if (!component) return m; + + if (currentMode === "tablet_landscape") { + return { + ...m, + components: { + ...m.components, + [componentId]: { ...component, position: newPosition }, + }, + }; + } else { + return { + ...m, + overrides: { + ...m.overrides, + [currentMode]: { + ...m.overrides?.[currentMode], + positions: { + ...m.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + }, + }, + }; + } + }), + }; + } + }); + setHasChanges(true); }, - [layout, currentMode] + [currentMode, activeCanvasId] ); const handleResizeEnd = useCallback( @@ -493,51 +552,87 @@ export default function PopDesigner({ // 컴포넌트가 자신의 rowSpan/colSpan을 동적으로 변경 요청 (CardList 확장 등) const handleRequestResize = useCallback( (componentId: string, newRowSpan: number, newColSpan?: number) => { - const component = layout.components[componentId]; - if (!component) return; + setLayout((prev) => { + const buildPosition = (comp: PopComponentDefinition) => ({ + ...comp.position, + rowSpan: newRowSpan, + ...(newColSpan !== undefined ? { colSpan: newColSpan } : {}), + }); - const newPosition = { - ...component.position, - rowSpan: newRowSpan, - ...(newColSpan !== undefined ? { colSpan: newColSpan } : {}), - }; - - // 기본 모드(tablet_landscape)인 경우: 원본 position 직접 수정 - if (currentMode === "tablet_landscape") { - const newLayout = { - ...layout, - components: { - ...layout.components, - [componentId]: { - ...component, - position: newPosition, - }, - }, - }; - setLayout(newLayout); - saveToHistory(newLayout); - setHasChanges(true); - } else { - // 다른 모드인 경우: 오버라이드에 저장 - const newLayout = { - ...layout, - overrides: { - ...layout.overrides, - [currentMode]: { - ...layout.overrides?.[currentMode], - positions: { - ...layout.overrides?.[currentMode]?.positions, - [componentId]: newPosition, + if (activeCanvasId === "main") { + const component = prev.components[componentId]; + if (!component) return prev; + const newPosition = buildPosition(component); + + if (currentMode === "tablet_landscape") { + const newLayout = { + ...prev, + components: { + ...prev.components, + [componentId]: { ...component, position: newPosition }, }, - }, - }, - }; - setLayout(newLayout); - saveToHistory(newLayout); - setHasChanges(true); - } + }; + saveToHistory(newLayout); + return newLayout; + } else { + const newLayout = { + ...prev, + overrides: { + ...prev.overrides, + [currentMode]: { + ...prev.overrides?.[currentMode], + positions: { + ...prev.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + }, + }, + }; + saveToHistory(newLayout); + return newLayout; + } + } else { + // 모달 캔버스 + const newLayout = { + ...prev, + modals: (prev.modals || []).map(m => { + if (m.id !== activeCanvasId) return m; + const component = m.components[componentId]; + if (!component) return m; + const newPosition = buildPosition(component); + + if (currentMode === "tablet_landscape") { + return { + ...m, + components: { + ...m.components, + [componentId]: { ...component, position: newPosition }, + }, + }; + } else { + return { + ...m, + overrides: { + ...m.overrides, + [currentMode]: { + ...m.overrides?.[currentMode], + positions: { + ...m.overrides?.[currentMode]?.positions, + [componentId]: newPosition, + }, + }, + }, + }; + } + }), + }; + saveToHistory(newLayout); + return newLayout; + } + }); + setHasChanges(true); }, - [layout, currentMode, saveToHistory] + [currentMode, saveToHistory, activeCanvasId] ); // ======================================== diff --git a/frontend/components/pop/designer/panels/ComponentPalette.tsx b/frontend/components/pop/designer/panels/ComponentPalette.tsx index 3817b54d..ddedc7d0 100644 --- a/frontend/components/pop/designer/panels/ComponentPalette.tsx +++ b/frontend/components/pop/designer/panels/ComponentPalette.tsx @@ -3,7 +3,7 @@ import { useDrag } from "react-dnd"; import { cn } from "@/lib/utils"; import { PopComponentType } from "../types/pop-layout"; -import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine, UserCircle, BarChart2 } from "lucide-react"; +import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine, UserCircle, BarChart2, ClipboardCheck } from "lucide-react"; import { DND_ITEM_TYPES } from "../constants"; // 컴포넌트 정의 @@ -93,6 +93,12 @@ const PALETTE_ITEMS: PaletteItem[] = [ icon: UserCircle, description: "사용자 프로필 / PC 전환 / 로그아웃", }, + { + type: "pop-work-detail", + label: "작업 상세", + icon: ClipboardCheck, + description: "공정별 체크리스트/검사/실적 상세 작업 화면", + }, ]; // 드래그 가능한 컴포넌트 아이템 diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 89b4a551..3af031b4 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -84,6 +84,7 @@ const COMPONENT_TYPE_LABELS: Record = { "pop-field": "입력", "pop-scanner": "스캐너", "pop-profile": "프로필", + "pop-work-detail": "작업 상세", }; // ======================================== diff --git a/frontend/components/pop/designer/types/pop-layout.ts b/frontend/components/pop/designer/types/pop-layout.ts index 7b008caf..f859cf5d 100644 --- a/frontend/components/pop/designer/types/pop-layout.ts +++ b/frontend/components/pop/designer/types/pop-layout.ts @@ -7,7 +7,7 @@ /** * POP 컴포넌트 타입 */ -export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-card-list-v2" | "pop-button" | "pop-string-list" | "pop-search" | "pop-status-bar" | "pop-field" | "pop-scanner" | "pop-profile"; +export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-card-list-v2" | "pop-button" | "pop-string-list" | "pop-search" | "pop-status-bar" | "pop-field" | "pop-scanner" | "pop-profile" | "pop-work-detail"; /** * 데이터 흐름 정의 @@ -377,6 +377,7 @@ export const DEFAULT_COMPONENT_GRID_SIZE: Record; } /** 열린 모달 상태 */ interface OpenModal { definition: PopModalDefinition; returnTo?: string; + fullscreen?: boolean; } // ======================================== @@ -61,10 +64,17 @@ export default function PopViewerWithModals({ currentMode, overrideGap, overridePadding, + parentRow, }: PopViewerWithModalsProps) { const router = useRouter(); const [modalStack, setModalStack] = useState([]); - const { subscribe, publish } = usePopEvent(screenId); + const { subscribe, publish, setSharedData } = usePopEvent(screenId); + + useEffect(() => { + if (parentRow) { + setSharedData("parentRow", parentRow); + } + }, [parentRow, setSharedData]); // 연결 해석기: layout에 정의된 connections를 이벤트 라우팅으로 변환 const stableConnections = useMemo( @@ -96,6 +106,7 @@ export default function PopViewerWithModals({ title?: string; mode?: string; returnTo?: string; + fullscreen?: boolean; }; if (data?.modalId) { @@ -104,6 +115,7 @@ export default function PopViewerWithModals({ setModalStack(prev => [...prev, { definition: modalDef, returnTo: data.returnTo, + fullscreen: data.fullscreen, }]); } } @@ -173,7 +185,7 @@ export default function PopViewerWithModals({ {/* 모달 스택 렌더링 */} {modalStack.map((modal, index) => { - const { definition } = modal; + const { definition, fullscreen } = modal; const isTopModal = index === modalStack.length - 1; const closeOnOverlay = definition.frameConfig?.closeOnOverlay !== false; const closeOnEsc = definition.frameConfig?.closeOnEsc !== false; @@ -185,10 +197,15 @@ export default function PopViewerWithModals({ overrides: definition.overrides, }; - const detectedMode = currentMode || detectGridMode(viewportWidth); - const modalWidth = resolveModalWidth(definition.sizeConfig, detectedMode, viewportWidth); - const isFull = modalWidth >= viewportWidth; - const rendererWidth = isFull ? viewportWidth : modalWidth - 32; + const isFull = fullscreen || (() => { + const detectedMode = currentMode || detectGridMode(viewportWidth); + const modalWidth = resolveModalWidth(definition.sizeConfig, detectedMode, viewportWidth); + return modalWidth >= viewportWidth; + })(); + const rendererWidth = isFull + ? viewportWidth + : resolveModalWidth(definition.sizeConfig, currentMode || detectGridMode(viewportWidth), viewportWidth) - 32; + const modalWidth = isFull ? viewportWidth : resolveModalWidth(definition.sizeConfig, currentMode || detectGridMode(viewportWidth), viewportWidth); return ( { - // 최상위 모달이 아니면 overlay 클릭 무시 (하위 모달이 먼저 닫히는 것 방지) if (!isTopModal || !closeOnOverlay) e.preventDefault(); }} onEscapeKeyDown={(e) => { if (!isTopModal || !closeOnEsc) e.preventDefault(); }} > - + {definition.title} diff --git a/frontend/lib/registry/pop-components/index.ts b/frontend/lib/registry/pop-components/index.ts index 351d6700..28e6a746 100644 --- a/frontend/lib/registry/pop-components/index.ts +++ b/frontend/lib/registry/pop-components/index.ts @@ -26,3 +26,4 @@ import "./pop-status-bar"; import "./pop-field"; import "./pop-scanner"; import "./pop-profile"; +import "./pop-work-detail"; diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 6d04d91c..8c3c6447 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -139,7 +139,7 @@ export function PopCardListV2Component({ currentColSpan, onRequestResize, }: PopCardListV2ComponentProps) { - const { subscribe, publish } = usePopEvent(screenId || "default"); + const { subscribe, publish, setSharedData } = usePopEvent(screenId || "default"); const router = useRouter(); const { userId: currentUserId } = useAuth(); @@ -250,6 +250,13 @@ export function PopCardListV2Component({ const [popModalRow, setPopModalRow] = useState(null); const openPopModal = useCallback(async (screenIdStr: string, row: RowData) => { + // 내부 모달 캔버스 (디자이너에서 생성한 modal-*)인 경우 이벤트 발행 + if (screenIdStr.startsWith("modal-")) { + setSharedData("parentRow", row); + publish("__pop_modal_open__", { modalId: screenIdStr, fullscreen: true }); + return; + } + // 외부 POP 화면 ID인 경우 기존 fetch 방식 try { const sid = parseInt(screenIdStr, 10); if (isNaN(sid)) { @@ -268,7 +275,7 @@ export function PopCardListV2Component({ } catch { toast.error("POP 화면을 불러오는데 실패했습니다."); } - }, []); + }, [publish, setSharedData]); const handleCardSelect = useCallback((row: RowData) => { @@ -1176,6 +1183,7 @@ export function PopCardListV2Component({ viewportWidth={typeof window !== "undefined" ? window.innerWidth : 1024} screenId={popModalScreenId} currentMode={detectGridMode(typeof window !== "undefined" ? window.innerWidth : 1024)} + parentRow={popModalRow ?? undefined} /> )}
diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx index 04ba0622..9fc1339a 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx @@ -12,6 +12,7 @@ import { useState, useEffect, useRef, useCallback, useMemo, Fragment } from "rea import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { usePopDesignerContext } from "@/components/pop/designer/PopDesignerContext"; import { Switch } from "@/components/ui/switch"; import { Select, @@ -2940,6 +2941,7 @@ function TabActions({ onUpdate: (partial: Partial) => void; columns: ColumnInfo[]; }) { + const designerCtx = usePopDesignerContext(); const overflow = cfg.overflow || { mode: "loadMore" as const, visibleCount: 6 }; const clickAction = cfg.cardClickAction || "none"; const modalConfig = cfg.cardClickModalConfig || { screenId: "" }; @@ -3013,15 +3015,52 @@ function TabActions({
{clickAction === "modal-open" && (
-
- POP 화면 ID - onUpdate({ cardClickModalConfig: { ...modalConfig, screenId: e.target.value } })} - placeholder="화면 ID (예: 4481)" - className="h-7 flex-1 text-[10px]" - /> -
+ {/* 모달 캔버스 (디자이너 모드) */} + {designerCtx && ( +
+ {modalConfig.screenId?.startsWith("modal-") ? ( + + ) : ( + + )} +
+ )} + {/* 뷰어 모드 또는 직접 입력 폴백 */} + {!designerCtx && ( +
+ 모달 ID + onUpdate({ cardClickModalConfig: { ...modalConfig, screenId: e.target.value } })} + placeholder="모달 ID" + className="h-7 flex-1 text-[10px]" + /> +
+ )}
모달 제목 ; + +interface WorkResultRow { + id: string; + work_order_process_id: string; + source_work_item_id: string; + source_detail_id: string; + work_phase: string; + item_title: string; + item_sort_order: string; + detail_type: string; + detail_label: string; + detail_sort_order: string; + spec_value: string | null; + lower_limit: string | null; + upper_limit: string | null; + input_type: string | null; + result_value: string | null; + status: string; + is_passed: string | null; + recorded_by: string | null; + recorded_at: string | null; +} + +interface WorkGroup { + phase: string; + title: string; + itemId: string; + sortOrder: number; + total: number; + completed: number; +} + +type WorkPhase = "PRE" | "IN" | "POST"; +const PHASE_ORDER: Record = { PRE: 1, IN: 2, POST: 3 }; + +interface ProcessTimerData { + started_at: string | null; + paused_at: string | null; + total_paused_time: string | null; + status: string; + good_qty: string | null; + defect_qty: string | null; +} + +// ======================================== +// Props +// ======================================== + +interface PopWorkDetailComponentProps { + config?: PopWorkDetailConfig; + screenId?: string; + componentId?: string; + currentRowSpan?: number; + currentColSpan?: number; +} + +// ======================================== +// 메인 컴포넌트 +// ======================================== + +export function PopWorkDetailComponent({ + config, + screenId, + componentId, +}: PopWorkDetailComponentProps) { + const { getSharedData } = usePopEvent(screenId || "default"); + const { user } = useAuth(); + + const cfg: PopWorkDetailConfig = { + showTimer: config?.showTimer ?? true, + showQuantityInput: config?.showQuantityInput ?? true, + phaseLabels: config?.phaseLabels ?? { PRE: "작업 전", IN: "작업 중", POST: "작업 후" }, + }; + + // parentRow에서 현재 공정 정보 추출 + const parentRow = getSharedData("parentRow"); + const processFlow = parentRow?.__processFlow__ as TimelineProcessStep[] | undefined; + const currentProcess = processFlow?.find((p) => p.isCurrent); + const workOrderProcessId = currentProcess?.processId + ? String(currentProcess.processId) + : undefined; + const processName = currentProcess?.processName ?? "공정 상세"; + + // ======================================== + // 상태 + // ======================================== + + const [allResults, setAllResults] = useState([]); + const [processData, setProcessData] = useState(null); + const [loading, setLoading] = useState(true); + const [selectedGroupId, setSelectedGroupId] = useState(null); + const [tick, setTick] = useState(Date.now()); + const [savingIds, setSavingIds] = useState>(new Set()); + + // 수량 입력 로컬 상태 + const [goodQty, setGoodQty] = useState(""); + const [defectQty, setDefectQty] = useState(""); + + // ======================================== + // D-FE1: 데이터 로드 + // ======================================== + + const fetchData = useCallback(async () => { + if (!workOrderProcessId) { + setLoading(false); + return; + } + + try { + setLoading(true); + + const [resultRes, processRes] = await Promise.all([ + dataApi.getTableData("process_work_result", { + size: 500, + filters: { work_order_process_id: workOrderProcessId }, + }), + dataApi.getTableData("work_order_process", { + size: 1, + filters: { id: workOrderProcessId }, + }), + ]); + + setAllResults((resultRes.data ?? []) as unknown as WorkResultRow[]); + + const proc = (processRes.data?.[0] ?? null) as ProcessTimerData | null; + setProcessData(proc); + if (proc) { + setGoodQty(proc.good_qty ?? ""); + setDefectQty(proc.defect_qty ?? ""); + } + } catch { + toast.error("데이터를 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, [workOrderProcessId]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // ======================================== + // D-FE2: 좌측 사이드바 - 작업항목 그룹핑 + // ======================================== + + const groups = useMemo(() => { + const map = new Map(); + for (const row of allResults) { + const key = row.source_work_item_id; + if (!map.has(key)) { + map.set(key, { + phase: row.work_phase, + title: row.item_title, + itemId: key, + sortOrder: parseInt(row.item_sort_order || "0", 10), + total: 0, + completed: 0, + }); + } + const g = map.get(key)!; + g.total++; + if (row.status === "completed") g.completed++; + } + return Array.from(map.values()).sort( + (a, b) => + (PHASE_ORDER[a.phase] ?? 9) - (PHASE_ORDER[b.phase] ?? 9) || + a.sortOrder - b.sortOrder + ); + }, [allResults]); + + // phase별로 그룹핑 + const groupsByPhase = useMemo(() => { + const result: Record = {}; + for (const g of groups) { + if (!result[g.phase]) result[g.phase] = []; + result[g.phase].push(g); + } + return result; + }, [groups]); + + // 첫 그룹 자동 선택 + useEffect(() => { + if (groups.length > 0 && !selectedGroupId) { + setSelectedGroupId(groups[0].itemId); + } + }, [groups, selectedGroupId]); + + // ======================================== + // D-FE3: 우측 체크리스트 + // ======================================== + + const currentItems = useMemo( + () => + allResults + .filter((r) => r.source_work_item_id === selectedGroupId) + .sort((a, b) => parseInt(a.detail_sort_order || "0", 10) - parseInt(b.detail_sort_order || "0", 10)), + [allResults, selectedGroupId] + ); + + const saveResultValue = useCallback( + async ( + rowId: string, + resultValue: string, + isPassed: string | null, + newStatus: string + ) => { + setSavingIds((prev) => new Set(prev).add(rowId)); + try { + await apiClient.post("/pop/execute-action", { + tasks: [ + { type: "data-update", targetTable: "process_work_result", targetColumn: "result_value", value: resultValue, items: [{ id: rowId }] }, + { type: "data-update", targetTable: "process_work_result", targetColumn: "status", value: newStatus, items: [{ id: rowId }] }, + ...(isPassed !== null + ? [{ type: "data-update", targetTable: "process_work_result", targetColumn: "is_passed", value: isPassed, items: [{ id: rowId }] }] + : []), + { type: "data-update", targetTable: "process_work_result", targetColumn: "recorded_by", value: user?.userId ?? "", items: [{ id: rowId }] }, + { type: "data-update", targetTable: "process_work_result", targetColumn: "recorded_at", value: new Date().toISOString(), items: [{ id: rowId }] }, + ], + data: { items: [{ id: rowId }], fieldValues: {} }, + }); + + setAllResults((prev) => + prev.map((r) => + r.id === rowId + ? { + ...r, + result_value: resultValue, + status: newStatus, + is_passed: isPassed, + recorded_by: user?.userId ?? null, + recorded_at: new Date().toISOString(), + } + : r + ) + ); + } catch { + toast.error("저장에 실패했습니다."); + } finally { + setSavingIds((prev) => { + const next = new Set(prev); + next.delete(rowId); + return next; + }); + } + }, + [user?.userId] + ); + + // ======================================== + // D-FE4: 타이머 + // ======================================== + + useEffect(() => { + if (!cfg.showTimer || !processData?.started_at) return; + const id = setInterval(() => setTick(Date.now()), 1000); + return () => clearInterval(id); + }, [cfg.showTimer, processData?.started_at]); + + const elapsedMs = useMemo(() => { + if (!processData?.started_at) return 0; + const now = tick; + const totalMs = now - new Date(processData.started_at).getTime(); + const pausedSec = parseInt(processData.total_paused_time || "0", 10); + const currentPauseMs = processData.paused_at + ? now - new Date(processData.paused_at).getTime() + : 0; + return Math.max(0, totalMs - pausedSec * 1000 - currentPauseMs); + }, [processData?.started_at, processData?.paused_at, processData?.total_paused_time, tick]); + + const formattedTime = useMemo(() => { + const totalSec = Math.floor(elapsedMs / 1000); + const h = String(Math.floor(totalSec / 3600)).padStart(2, "0"); + const m = String(Math.floor((totalSec % 3600) / 60)).padStart(2, "0"); + const s = String(totalSec % 60).padStart(2, "0"); + return `${h}:${m}:${s}`; + }, [elapsedMs]); + + const isPaused = !!processData?.paused_at; + const isStarted = !!processData?.started_at; + + const handleTimerAction = useCallback( + async (action: "start" | "pause" | "resume") => { + if (!workOrderProcessId) return; + try { + await apiClient.post("/api/pop/production/timer", { + workOrderProcessId, + action, + }); + // 타이머 상태 새로고침 + const res = await dataApi.getTableData("work_order_process", { + size: 1, + filters: { id: workOrderProcessId }, + }); + const proc = (res.data?.[0] ?? null) as ProcessTimerData | null; + if (proc) setProcessData(proc); + } catch { + toast.error("타이머 제어에 실패했습니다."); + } + }, + [workOrderProcessId] + ); + + // ======================================== + // D-FE5: 수량 등록 + 완료 + // ======================================== + + const handleQuantityRegister = useCallback(async () => { + if (!workOrderProcessId) return; + try { + await apiClient.post("/pop/execute-action", { + tasks: [ + { type: "data-update", targetTable: "work_order_process", targetColumn: "good_qty", value: goodQty || "0", items: [{ id: workOrderProcessId }] }, + { type: "data-update", targetTable: "work_order_process", targetColumn: "defect_qty", value: defectQty || "0", items: [{ id: workOrderProcessId }] }, + ], + data: { items: [{ id: workOrderProcessId }], fieldValues: {} }, + }); + toast.success("수량이 등록되었습니다."); + } catch { + toast.error("수량 등록에 실패했습니다."); + } + }, [workOrderProcessId, goodQty, defectQty]); + + const handleProcessComplete = useCallback(async () => { + if (!workOrderProcessId) return; + try { + await apiClient.post("/pop/execute-action", { + tasks: [ + { type: "data-update", targetTable: "work_order_process", targetColumn: "status", value: "completed", items: [{ id: workOrderProcessId }] }, + { type: "data-update", targetTable: "work_order_process", targetColumn: "completed_at", value: new Date().toISOString(), items: [{ id: workOrderProcessId }] }, + ], + data: { items: [{ id: workOrderProcessId }], fieldValues: {} }, + }); + toast.success("공정이 완료되었습니다."); + setProcessData((prev) => + prev ? { ...prev, status: "completed" } : prev + ); + } catch { + toast.error("공정 완료 처리에 실패했습니다."); + } + }, [workOrderProcessId]); + + // ======================================== + // 안전 장치 + // ======================================== + + if (!parentRow) { + return ( +
+ + 카드를 선택해주세요 +
+ ); + } + + if (!workOrderProcessId) { + return ( +
+ + 공정 정보를 찾을 수 없습니다 +
+ ); + } + + if (loading) { + return ( +
+ +
+ ); + } + + if (allResults.length === 0) { + return ( +
+ + 작업기준이 등록되지 않았습니다 +
+ ); + } + + const isProcessCompleted = processData?.status === "completed"; + + // ======================================== + // 렌더링 + // ======================================== + + return ( +
+ {/* 헤더 */} +
+

{processName}

+ {cfg.showTimer && ( +
+ + + {formattedTime} + + {!isProcessCompleted && ( + <> + {!isStarted && ( + + )} + {isStarted && !isPaused && ( + + )} + {isStarted && isPaused && ( + + )} + + )} +
+ )} +
+ + {/* 본문: 좌측 사이드바 + 우측 체크리스트 */} +
+ {/* 좌측 사이드바 */} +
+ {(["PRE", "IN", "POST"] as WorkPhase[]).map((phase) => { + const phaseGroups = groupsByPhase[phase]; + if (!phaseGroups || phaseGroups.length === 0) return null; + return ( +
+
+ {cfg.phaseLabels[phase] ?? phase} +
+ {phaseGroups.map((g) => ( + + ))} +
+ ); + })} +
+ + {/* 우측 체크리스트 */} +
+ {selectedGroupId && ( +
+ {currentItems.map((item) => ( + + ))} +
+ )} +
+
+ + {/* 하단: 수량 입력 + 완료 */} + {cfg.showQuantityInput && ( +
+ +
+ 양품 + setGoodQty(e.target.value)} + disabled={isProcessCompleted} + /> +
+
+ 불량 + setDefectQty(e.target.value)} + disabled={isProcessCompleted} + /> +
+ +
+ {!isProcessCompleted && ( + + )} + {isProcessCompleted && ( + + 완료됨 + + )} +
+ )} +
+ ); +} + +// ======================================== +// 체크리스트 개별 항목 +// ======================================== + +interface ChecklistItemProps { + item: WorkResultRow; + saving: boolean; + disabled: boolean; + onSave: ( + rowId: string, + resultValue: string, + isPassed: string | null, + newStatus: string + ) => void; +} + +function ChecklistItem({ item, saving, disabled, onSave }: ChecklistItemProps) { + const isSaving = saving; + const isDisabled = disabled || isSaving; + + switch (item.detail_type) { + case "check": + return ; + case "inspect": + return ; + case "input": + return ; + case "procedure": + return ; + case "material": + return ; + default: + return ( +
+ 알 수 없는 유형: {item.detail_type} +
+ ); + } +} + +// ===== check: 체크박스 ===== + +function CheckItem({ + item, + disabled, + saving, + onSave, +}: { + item: WorkResultRow; + disabled: boolean; + saving: boolean; + onSave: ChecklistItemProps["onSave"]; +}) { + const checked = item.result_value === "Y"; + return ( +
+ { + const val = v ? "Y" : "N"; + onSave(item.id, val, v ? "Y" : "N", v ? "completed" : "pending"); + }} + /> + {item.detail_label} + {saving && } + {item.status === "completed" && !saving && ( + + 완료 + + )} +
+ ); +} + +// ===== inspect: 측정값 입력 (범위 판정) ===== + +function InspectItem({ + item, + disabled, + saving, + onSave, +}: { + item: WorkResultRow; + disabled: boolean; + saving: boolean; + onSave: ChecklistItemProps["onSave"]; +}) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const lower = parseFloat(item.lower_limit ?? ""); + const upper = parseFloat(item.upper_limit ?? ""); + const hasRange = !isNaN(lower) && !isNaN(upper); + + const handleBlur = () => { + if (!inputVal || disabled) return; + const numVal = parseFloat(inputVal); + let passed: string | null = null; + if (hasRange) { + passed = numVal >= lower && numVal <= upper ? "Y" : "N"; + } + onSave(item.id, inputVal, passed, "completed"); + }; + + const isPassed = item.is_passed; + + return ( +
+
+ {item.detail_label} + {hasRange && ( + + 기준: {item.lower_limit} ~ {item.upper_limit} + {item.spec_value ? ` (표준: ${item.spec_value})` : ""} + + )} +
+
+ setInputVal(e.target.value)} + onBlur={handleBlur} + disabled={disabled} + placeholder="측정값 입력" + /> + {saving && } + {isPassed === "Y" && !saving && ( + 합격 + )} + {isPassed === "N" && !saving && ( + 불합격 + )} +
+
+ ); +} + +// ===== input: 자유 입력 ===== + +function InputItem({ + item, + disabled, + saving, + onSave, +}: { + item: WorkResultRow; + disabled: boolean; + saving: boolean; + onSave: ChecklistItemProps["onSave"]; +}) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + const inputType = item.input_type === "number" ? "number" : "text"; + + const handleBlur = () => { + if (!inputVal || disabled) return; + onSave(item.id, inputVal, null, "completed"); + }; + + return ( +
+
{item.detail_label}
+
+ setInputVal(e.target.value)} + onBlur={handleBlur} + disabled={disabled} + placeholder="값 입력" + /> + {saving && } +
+
+ ); +} + +// ===== procedure: 절차 확인 (읽기 전용 + 체크) ===== + +function ProcedureItem({ + item, + disabled, + saving, + onSave, +}: { + item: WorkResultRow; + disabled: boolean; + saving: boolean; + onSave: ChecklistItemProps["onSave"]; +}) { + const checked = item.result_value === "Y"; + return ( +
+
+ {item.spec_value || item.detail_label} +
+
+ { + onSave(item.id, v ? "Y" : "N", null, v ? "completed" : "pending"); + }} + /> + 확인 + {saving && } +
+
+ ); +} + +// ===== material: 자재/LOT 입력 ===== + +function MaterialItem({ + item, + disabled, + saving, + onSave, +}: { + item: WorkResultRow; + disabled: boolean; + saving: boolean; + onSave: ChecklistItemProps["onSave"]; +}) { + const [inputVal, setInputVal] = useState(item.result_value ?? ""); + + const handleBlur = () => { + if (!inputVal || disabled) return; + onSave(item.id, inputVal, null, "completed"); + }; + + return ( +
+
{item.detail_label}
+
+ setInputVal(e.target.value)} + onBlur={handleBlur} + disabled={disabled} + placeholder="LOT 번호 입력" + /> + {saving && } +
+
+ ); +} diff --git a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx new file mode 100644 index 00000000..7b75cf78 --- /dev/null +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailConfig.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import type { PopWorkDetailConfig } from "../types"; + +interface PopWorkDetailConfigPanelProps { + config?: PopWorkDetailConfig; + onChange?: (config: PopWorkDetailConfig) => void; +} + +const DEFAULT_PHASE_LABELS: Record = { + PRE: "작업 전", + IN: "작업 중", + POST: "작업 후", +}; + +export function PopWorkDetailConfigPanel({ + config, + onChange, +}: PopWorkDetailConfigPanelProps) { + const cfg: PopWorkDetailConfig = { + showTimer: config?.showTimer ?? true, + showQuantityInput: config?.showQuantityInput ?? true, + phaseLabels: config?.phaseLabels ?? { ...DEFAULT_PHASE_LABELS }, + }; + + const update = (partial: Partial) => { + onChange?.({ ...cfg, ...partial }); + }; + + return ( +
+
+ + update({ showTimer: v })} + /> +
+ +
+ + update({ showQuantityInput: v })} + /> +
+ +
+ + {(["PRE", "IN", "POST"] as const).map((phase) => ( +
+ + {phase} + + + update({ + phaseLabels: { ...cfg.phaseLabels, [phase]: e.target.value }, + }) + } + /> +
+ ))} +
+
+ ); +} diff --git a/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailPreview.tsx b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailPreview.tsx new file mode 100644 index 00000000..d5eed206 --- /dev/null +++ b/frontend/lib/registry/pop-components/pop-work-detail/PopWorkDetailPreview.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { ClipboardCheck } from "lucide-react"; +import type { PopWorkDetailConfig } from "../types"; + +interface PopWorkDetailPreviewProps { + config?: PopWorkDetailConfig; +} + +export function PopWorkDetailPreviewComponent({ config }: PopWorkDetailPreviewProps) { + const labels = config?.phaseLabels ?? { PRE: "작업 전", IN: "작업 중", POST: "작업 후" }; + return ( +
+ + + 작업 상세 + +
+ {Object.values(labels).map((l) => ( + + {l} + + ))} +
+
+ ); +} diff --git a/frontend/lib/registry/pop-components/pop-work-detail/index.tsx b/frontend/lib/registry/pop-components/pop-work-detail/index.tsx new file mode 100644 index 00000000..941db8d4 --- /dev/null +++ b/frontend/lib/registry/pop-components/pop-work-detail/index.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { PopComponentRegistry } from "../../PopComponentRegistry"; +import { PopWorkDetailComponent } from "./PopWorkDetailComponent"; +import { PopWorkDetailConfigPanel } from "./PopWorkDetailConfig"; +import { PopWorkDetailPreviewComponent } from "./PopWorkDetailPreview"; +import type { PopWorkDetailConfig } from "../types"; + +const defaultConfig: PopWorkDetailConfig = { + showTimer: true, + showQuantityInput: true, + phaseLabels: { PRE: "작업 전", IN: "작업 중", POST: "작업 후" }, +}; + +PopComponentRegistry.registerComponent({ + id: "pop-work-detail", + name: "작업 상세", + description: "공정별 체크리스트/검사/실적 상세 작업 화면", + category: "display", + icon: "ClipboardCheck", + component: PopWorkDetailComponent, + configPanel: PopWorkDetailConfigPanel, + preview: PopWorkDetailPreviewComponent, + defaultProps: defaultConfig, + connectionMeta: { + sendable: [ + { + key: "process_completed", + label: "공정 완료", + type: "event", + category: "event", + description: "공정 작업 전체 완료 이벤트", + }, + ], + receivable: [], + }, + touchOptimized: true, + supportedDevices: ["mobile", "tablet"], +}); diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts index 3680578e..a32a53cd 100644 --- a/frontend/lib/registry/pop-components/types.ts +++ b/frontend/lib/registry/pop-components/types.ts @@ -1000,3 +1000,14 @@ export const VIRTUAL_SUB_STATUS = "__subStatus__" as const; export const VIRTUAL_SUB_SEMANTIC = "__subSemantic__" as const; export const VIRTUAL_SUB_PROCESS = "__subProcessName__" as const; export const VIRTUAL_SUB_SEQ = "__subSeqNo__" as const; + + +// ============================================= +// pop-work-detail 전용 타입 +// ============================================= + +export interface PopWorkDetailConfig { + showTimer: boolean; + showQuantityInput: boolean; + phaseLabels: Record; +} From 3225a7bb21db8b3f7a81961baf46f75fa87c5131 Mon Sep 17 00:00:00 2001 From: syc0123 Date: Mon, 16 Mar 2026 10:38:12 +0900 Subject: [PATCH 14/18] feat: add bulk update script for COMPANY_7 button styles - Introduced a new script `btn-bulk-update-company7.ts` to facilitate bulk updates of button styles for COMPANY_7. - The script includes functionalities for testing, running updates, creating backups, and restoring from backups. - Implemented logic to dynamically apply button styles based on action types, ensuring consistent UI across the application. - Updated documentation to reflect changes in button icon mapping and dynamic loading of icons. This addition enhances the maintainability and consistency of button styles for COMPANY_7, streamlining the update process. --- .../scripts/btn-bulk-update-company7.ts | 318 ++++++++++++++++++ docs/ycshin-node/BIC[계획]-버튼-아이콘화.md | 51 ++- docs/ycshin-node/BIC[맥락]-버튼-아이콘화.md | 44 ++- docs/ycshin-node/BIC[체크]-버튼-아이콘화.md | 25 +- .../BTN-일괄변경-탑씰-버튼스타일.md | 171 ++++++++++ .../MPN[체크]-품번-수동접두어채번.md | 19 +- .../config-panels/ButtonConfigPanel.tsx | 13 +- frontend/lib/button-icon-map.tsx | 17 +- .../ButtonPrimaryComponent.tsx | 19 +- .../components/v2-button-primary/config.ts | 25 +- .../components/v2-button-primary/index.ts | 18 +- 11 files changed, 678 insertions(+), 42 deletions(-) create mode 100644 backend-node/scripts/btn-bulk-update-company7.ts create mode 100644 docs/ycshin-node/BTN-일괄변경-탑씰-버튼스타일.md diff --git a/backend-node/scripts/btn-bulk-update-company7.ts b/backend-node/scripts/btn-bulk-update-company7.ts new file mode 100644 index 00000000..ee757a0c --- /dev/null +++ b/backend-node/scripts/btn-bulk-update-company7.ts @@ -0,0 +1,318 @@ +/** + * 탑씰(company_7) 버튼 스타일 일괄 변경 스크립트 + * + * 사용법: + * npx ts-node scripts/btn-bulk-update-company7.ts --test # 1건만 테스트 (ROLLBACK) + * npx ts-node scripts/btn-bulk-update-company7.ts --run # 전체 실행 (COMMIT) + * npx ts-node scripts/btn-bulk-update-company7.ts --backup # 백업 테이블만 생성 + * npx ts-node scripts/btn-bulk-update-company7.ts --restore # 백업에서 원복 + */ + +import { Pool } from "pg"; + +// ── 배포 DB 연결 ── +const pool = new Pool({ + connectionString: + "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/vexplor", +}); + +const COMPANY_CODE = "COMPANY_7"; +const BACKUP_TABLE = "screen_layouts_v2_backup_20260313"; + +// ── 액션별 기본 아이콘 매핑 (frontend/lib/button-icon-map.tsx 기준) ── +const actionIconMap: Record = { + save: "Check", + delete: "Trash2", + edit: "Pencil", + navigate: "ArrowRight", + modal: "Maximize2", + transferData: "SendHorizontal", + excel_download: "Download", + excel_upload: "Upload", + quickInsert: "Zap", + control: "Settings", + barcode_scan: "ScanLine", + operation_control: "Truck", + event: "Send", + copy: "Copy", +}; +const FALLBACK_ICON = "SquareMousePointer"; + +function getIconForAction(actionType?: string): string { + if (actionType && actionIconMap[actionType]) { + return actionIconMap[actionType]; + } + return FALLBACK_ICON; +} + +// ── 버튼 컴포넌트인지 판별 (최상위 + 탭 내부 둘 다 지원) ── +function isTopLevelButton(comp: any): boolean { + return ( + comp.url?.includes("v2-button-primary") || + comp.overrides?.type === "v2-button-primary" + ); +} + +function isTabChildButton(comp: any): boolean { + return comp.componentType === "v2-button-primary"; +} + +function isButtonComponent(comp: any): boolean { + return isTopLevelButton(comp) || isTabChildButton(comp); +} + +// ── 탭 위젯인지 판별 ── +function isTabsWidget(comp: any): boolean { + return ( + comp.url?.includes("v2-tabs-widget") || + comp.overrides?.type === "v2-tabs-widget" + ); +} + +// ── 버튼 스타일 변경 (최상위 버튼용: overrides 사용) ── +function applyButtonStyle(config: any, actionType: string | undefined) { + const iconName = getIconForAction(actionType); + + config.displayMode = "icon-text"; + + config.icon = { + name: iconName, + type: "lucide", + size: "보통", + ...(config.icon?.color ? { color: config.icon.color } : {}), + }; + + config.iconTextPosition = "right"; + config.iconGap = 6; + + if (!config.style) config.style = {}; + delete config.style.width; // 레거시 하드코딩 너비 제거 (size.width만 사용) + config.style.borderRadius = "8px"; + config.style.labelColor = "#FFFFFF"; + config.style.fontSize = "12px"; + config.style.fontWeight = "normal"; + config.style.labelTextAlign = "left"; + + if (actionType === "delete") { + config.style.backgroundColor = "#F04544"; + } else if (actionType === "excel_upload" || actionType === "excel_download") { + config.style.backgroundColor = "#212121"; + } else { + config.style.backgroundColor = "#3B83F6"; + } +} + +function updateButtonStyle(comp: any): boolean { + if (isTopLevelButton(comp)) { + const overrides = comp.overrides || {}; + const actionType = overrides.action?.type; + + if (!comp.size) comp.size = {}; + comp.size.height = 40; + + applyButtonStyle(overrides, actionType); + comp.overrides = overrides; + return true; + } + + if (isTabChildButton(comp)) { + const config = comp.componentConfig || {}; + const actionType = config.action?.type; + + if (!comp.size) comp.size = {}; + comp.size.height = 40; + + applyButtonStyle(config, actionType); + comp.componentConfig = config; + + // 탭 내부 버튼은 렌더러가 comp.style (최상위)에서 스타일을 읽음 + if (!comp.style) comp.style = {}; + comp.style.borderRadius = "8px"; + comp.style.labelColor = "#FFFFFF"; + comp.style.fontSize = "12px"; + comp.style.fontWeight = "normal"; + comp.style.labelTextAlign = "left"; + comp.style.backgroundColor = config.style.backgroundColor; + + return true; + } + + return false; +} + +// ── 백업 테이블 생성 ── +async function createBackup() { + console.log(`\n=== 백업 테이블 생성: ${BACKUP_TABLE} ===`); + + const exists = await pool.query( + `SELECT to_regclass($1) AS tbl`, + [BACKUP_TABLE], + ); + if (exists.rows[0].tbl) { + console.log(`백업 테이블이 이미 존재합니다: ${BACKUP_TABLE}`); + const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`); + console.log(`기존 백업 레코드 수: ${count.rows[0].count}`); + return; + } + + await pool.query( + `CREATE TABLE ${BACKUP_TABLE} AS + SELECT * FROM screen_layouts_v2 + WHERE company_code = $1`, + [COMPANY_CODE], + ); + + const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`); + console.log(`백업 완료. 레코드 수: ${count.rows[0].count}`); +} + +// ── 백업에서 원복 ── +async function restoreFromBackup() { + console.log(`\n=== 백업에서 원복: ${BACKUP_TABLE} ===`); + + const result = await pool.query( + `UPDATE screen_layouts_v2 AS target + SET layout_data = backup.layout_data, + updated_at = backup.updated_at + FROM ${BACKUP_TABLE} AS backup + WHERE target.screen_id = backup.screen_id + AND target.company_code = backup.company_code + AND target.layer_id = backup.layer_id`, + ); + console.log(`원복 완료. 변경된 레코드 수: ${result.rowCount}`); +} + +// ── 메인: 버튼 일괄 변경 ── +async function updateButtons(testMode: boolean) { + const modeLabel = testMode ? "테스트 (1건, ROLLBACK)" : "전체 실행 (COMMIT)"; + console.log(`\n=== 버튼 일괄 변경 시작 [${modeLabel}] ===`); + + // company_7 레코드 조회 + const rows = await pool.query( + `SELECT screen_id, layer_id, company_code, layout_data + FROM screen_layouts_v2 + WHERE company_code = $1 + ORDER BY screen_id, layer_id`, + [COMPANY_CODE], + ); + console.log(`대상 레코드 수: ${rows.rowCount}`); + + if (!rows.rowCount) { + console.log("변경할 레코드가 없습니다."); + return; + } + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + let totalUpdated = 0; + let totalButtons = 0; + const targetRows = testMode ? [rows.rows[0]] : rows.rows; + + for (const row of targetRows) { + const layoutData = row.layout_data; + if (!layoutData?.components || !Array.isArray(layoutData.components)) { + continue; + } + + let buttonsInRow = 0; + for (const comp of layoutData.components) { + // 최상위 버튼 처리 + if (updateButtonStyle(comp)) { + buttonsInRow++; + } + + // 탭 위젯 내부 버튼 처리 + if (isTabsWidget(comp)) { + const tabs = comp.overrides?.tabs || []; + for (const tab of tabs) { + const tabComps = tab.components || []; + for (const tabComp of tabComps) { + if (updateButtonStyle(tabComp)) { + buttonsInRow++; + } + } + } + } + } + + if (buttonsInRow > 0) { + await client.query( + `UPDATE screen_layouts_v2 + SET layout_data = $1, updated_at = NOW() + WHERE screen_id = $2 AND company_code = $3 AND layer_id = $4`, + [JSON.stringify(layoutData), row.screen_id, row.company_code, row.layer_id], + ); + totalUpdated++; + totalButtons += buttonsInRow; + + console.log( + ` screen_id=${row.screen_id}, layer_id=${row.layer_id} → 버튼 ${buttonsInRow}개 변경`, + ); + + // 테스트 모드: 변경 전후 비교를 위해 첫 번째 버튼 출력 + if (testMode) { + const sampleBtn = layoutData.components.find(isButtonComponent); + if (sampleBtn) { + console.log("\n--- 변경 후 샘플 버튼 ---"); + console.log(JSON.stringify(sampleBtn, null, 2)); + } + } + } + } + + console.log(`\n--- 결과 ---`); + console.log(`변경된 레코드: ${totalUpdated}개`); + console.log(`변경된 버튼: ${totalButtons}개`); + + if (testMode) { + await client.query("ROLLBACK"); + console.log("\n[테스트 모드] ROLLBACK 완료. 실제 DB 변경 없음."); + } else { + await client.query("COMMIT"); + console.log("\nCOMMIT 완료."); + } + } catch (err) { + await client.query("ROLLBACK"); + console.error("\n에러 발생. ROLLBACK 완료.", err); + throw err; + } finally { + client.release(); + } +} + +// ── CLI 진입점 ── +async function main() { + const arg = process.argv[2]; + + if (!arg || !["--test", "--run", "--backup", "--restore"].includes(arg)) { + console.log("사용법:"); + console.log(" --test : 1건 테스트 (ROLLBACK, DB 변경 없음)"); + console.log(" --run : 전체 실행 (COMMIT)"); + console.log(" --backup : 백업 테이블 생성"); + console.log(" --restore : 백업에서 원복"); + process.exit(1); + } + + try { + if (arg === "--backup") { + await createBackup(); + } else if (arg === "--restore") { + await restoreFromBackup(); + } else if (arg === "--test") { + await createBackup(); + await updateButtons(true); + } else if (arg === "--run") { + await createBackup(); + await updateButtons(false); + } + } catch (err) { + console.error("스크립트 실행 실패:", err); + process.exit(1); + } finally { + await pool.end(); + } +} + +main(); diff --git a/docs/ycshin-node/BIC[계획]-버튼-아이콘화.md b/docs/ycshin-node/BIC[계획]-버튼-아이콘화.md index 816eaa1e..be3a3776 100644 --- a/docs/ycshin-node/BIC[계획]-버튼-아이콘화.md +++ b/docs/ycshin-node/BIC[계획]-버튼-아이콘화.md @@ -323,7 +323,7 @@ interface ButtonComponentConfig { | 파일 | 내용 | |------|------| -| `frontend/lib/button-icon-map.ts` | 버튼 액션별 추천 아이콘 매핑 + 아이콘 동적 렌더링 유틸 | +| `frontend/lib/button-icon-map.tsx` | 버튼 액션별 추천 아이콘 매핑 + 아이콘 동적 렌더링 유틸 | --- @@ -338,3 +338,52 @@ interface ButtonComponentConfig { - 외부 SVG 붙여넣기도 지원 → 관리자가 회사 로고 등 자체 아이콘을 등록 가능 - lucide 커스텀 아이콘은 `componentConfig.customIcons`에, SVG 아이콘은 `componentConfig.customSvgIcons`에 저장 - lucide 아이콘 렌더링: 아이콘 이름 → 컴포넌트 매핑, SVG 아이콘 렌더링: `dangerouslySetInnerHTML` + DOMPurify 정화 +- **동적 아이콘 로딩**: `iconMap`에 명시적으로 import되지 않은 lucide 아이콘도 `getLucideIcon()` 호출 시 `lucide-react`의 전체 아이콘(`icons`)에서 자동 조회 후 캐싱 → 화면 관리에서 선택한 모든 lucide 아이콘이 실제 화면에서도 렌더링됨 +- **커스텀 아이콘 전역 관리 (미구현)**: 커스텀 아이콘을 버튼별(`componentConfig`)이 아닌 시스템 전역(`custom_icon_registry` 테이블)으로 관리하여, 한번 추가한 커스텀 아이콘이 모든 화면의 모든 버튼에서 사용 가능하도록 확장 예정 + +--- + +## [미구현] 커스텀 아이콘 전역 관리 + +### 현재 문제 + +- 커스텀 아이콘이 `componentConfig.customIcons`에 저장 → **해당 버튼에서만** 보임 +- 저장1 버튼에 추가한 커스텀 아이콘이 저장2 버튼, 다른 화면에서는 안 보임 +- 같은 아이콘을 쓰려면 매번 검색해서 다시 추가해야 함 + +### 변경 후 동작 + +- 커스텀 아이콘을 **회사(company_code) 단위 전역**으로 관리 +- 어떤 화면의 어떤 버튼에서든 커스텀 아이콘 추가 → 모든 화면의 모든 버튼에서 커스텀란에 표시 +- 버튼 액션 종류와 무관하게 모든 커스텀 아이콘이 노출 + +### DB 테이블 (신규) + +```sql +CREATE TABLE custom_icon_registry ( + id VARCHAR(500) PRIMARY KEY DEFAULT gen_random_uuid()::text, + company_code VARCHAR(500) NOT NULL, + icon_name VARCHAR(500) NOT NULL, + icon_type VARCHAR(500) DEFAULT 'lucide', -- 'lucide' | 'svg' + svg_data TEXT, -- SVG일 경우 원본 데이터 + created_date TIMESTAMP DEFAULT now(), + updated_date TIMESTAMP DEFAULT now(), + writer VARCHAR(500) +); + +CREATE INDEX idx_custom_icon_registry_company ON custom_icon_registry(company_code); +``` + +### 백엔드 API (신규) + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| GET | `/api/custom-icons` | 커스텀 아이콘 목록 조회 (company_code 필터) | +| POST | `/api/custom-icons` | 커스텀 아이콘 추가 | +| DELETE | `/api/custom-icons/:id` | 커스텀 아이콘 삭제 | + +### 프론트엔드 변경 + +- `ButtonConfigPanel` — 커스텀 아이콘 조회/추가/삭제를 API 호출로 변경 +- 기존 `componentConfig.customIcons` 데이터는 하위 호환으로 병합 표시 (점진적 마이그레이션) +- `componentConfig.customSvgIcons`도 동일하게 전역 테이블로 이관 diff --git a/docs/ycshin-node/BIC[맥락]-버튼-아이콘화.md b/docs/ycshin-node/BIC[맥락]-버튼-아이콘화.md index f4b2b16d..ba19e386 100644 --- a/docs/ycshin-node/BIC[맥락]-버튼-아이콘화.md +++ b/docs/ycshin-node/BIC[맥락]-버튼-아이콘화.md @@ -145,8 +145,24 @@ - **결정**: lucide-react에서 export되는 전체 아이콘 이름 목록을 검색 가능 - **근거**: 관리자가 "어떤 아이콘이 있는지" 모르므로 검색 기능이 필수 -- **구현**: lucide 아이콘 이름 배열을 상수로 관리하고, CommandInput으로 필터링 -- **주의**: 전체 아이콘 컴포넌트를 import하지 않고, 이름 배열만 관리 → 선택 시에만 해당 아이콘을 매핑에 추가 +- **구현**: `lucide-react`의 `icons` 객체에서 `Object.keys()`로 전체 이름 목록을 가져오고, CommandInput으로 필터링 +- **주의**: `allLucideIcons`는 `button-icon-map.tsx`에서 re-export하여 import를 중앙화 + +### 18. 커스텀 아이콘 전역 관리 (미구현) + +- **결정**: 커스텀 아이콘을 버튼별(`componentConfig`) → 시스템 전역(`custom_icon_registry` 테이블)으로 변경 +- **근거**: 현재는 버튼 A에서 추가한 커스텀 아이콘이 버튼 B, 다른 화면에서 안 보여 매번 재등록 필요. 아이콘은 시각적 자원이므로 액션이나 화면에 종속될 이유가 없음 +- **범위 검토**: 버튼별 < 화면 단위 < **시스템 전역(채택)** — 같은 아이콘을 여러 화면에서 재사용하는 ERP 특성에 시스템 전역이 가장 적합 +- **저장**: `custom_icon_registry` 테이블 (company_code 멀티테넌시), lucide 이름 또는 SVG 데이터 저장 +- **하위 호환**: 기존 `componentConfig.customIcons` 데이터는 병합 표시 후 점진적 마이그레이션 + +### 19. 동적 아이콘 로딩 (getLucideIcon fallback) + +- **결정**: `getLucideIcon(name)`이 `iconMap`에 없는 아이콘을 `lucide-react`의 `icons` 전체 객체에서 동적으로 조회 후 캐싱 +- **근거**: 화면 관리에서 커스텀 lucide 아이콘을 선택하면 `componentConfig.customIcons`에 이름만 저장됨. 디자이너 세션에서는 `addToIconMap()`으로 런타임에 등록되지만, 실제 화면(뷰어) 로드 시에는 `iconMap`에 해당 아이콘이 없어 렌더링 실패. `icons` fallback을 추가하면 **어떤 lucide 아이콘이든 이름만으로 자동 렌더링** +- **구현**: `button-icon-map.tsx`에 `import { icons as allLucideIcons } from "lucide-react"` 추가, `getLucideIcon()`에서 `iconMap` miss 시 `allLucideIcons[name]` 조회 후 `iconMap`에 캐싱 +- **번들 영향**: `icons` 전체 객체 import로 번들 크기 증가 (~100-200KB). ERP 애플리케이션 특성상 수용 가능한 수준이며, 관리자가 선택한 모든 아이콘이 실제 화면에서 동작하는 것이 더 중요 +- **대안 검토**: 뷰어 로드 시 `customIcons`를 순회하여 개별 등록 → 기각 (모든 뷰어 컴포넌트에 로직 추가 필요, 누락 위험) --- @@ -159,7 +175,7 @@ | 뷰어 렌더링 (수정) | `frontend/components/screen/InteractiveScreenViewer.tsx` | 버튼 렌더링 분기 (2041~2059행) | | 위젯 (수정) | `frontend/components/screen/widgets/types/ButtonWidget.tsx` | 위젯 기반 버튼 렌더링 (67~86행) | | 최적화 버튼 (수정) | `frontend/components/screen/OptimizedButtonComponent.tsx` | 최적화된 버튼 렌더링 (643~674행) | -| 아이콘 매핑 (신규) | `frontend/lib/button-icon-map.ts` | 액션별 추천 아이콘 + 동적 렌더링 유틸 | +| 아이콘 매핑 (신규) | `frontend/lib/button-icon-map.tsx` | 액션별 추천 아이콘 + 동적 렌더링 유틸 + allLucideIcons fallback | | 타입 정의 (참고) | `frontend/types/screen.ts` | ComponentData, componentConfig 타입 | --- @@ -169,17 +185,21 @@ ### lucide-react 아이콘 동적 렌더링 ```typescript -// button-icon-map.ts -import { Check, Save, Trash2, Pencil, ... } from "lucide-react"; +// button-icon-map.tsx +import { Check, Save, ..., icons as allLucideIcons, type LucideIcon } from "lucide-react"; -const iconMap: Record> = { - Check, Save, Trash2, Pencil, ... -}; +// 추천 아이콘은 명시적 import, 나머지는 동적 조회 +const iconMap: Record = { Check, Save, ... }; -export function renderButtonIcon(name: string, size: string | number) { - const IconComponent = iconMap[name]; - if (!IconComponent) return null; - return ; +export function getLucideIcon(name: string): LucideIcon | undefined { + if (iconMap[name]) return iconMap[name]; + // iconMap에 없으면 lucide-react 전체에서 동적 조회 후 캐싱 + const found = allLucideIcons[name as keyof typeof allLucideIcons]; + if (found) { + iconMap[name] = found; + return found; + } + return undefined; } ``` diff --git a/docs/ycshin-node/BIC[체크]-버튼-아이콘화.md b/docs/ycshin-node/BIC[체크]-버튼-아이콘화.md index a02a15b1..1b20cab9 100644 --- a/docs/ycshin-node/BIC[체크]-버튼-아이콘화.md +++ b/docs/ycshin-node/BIC[체크]-버튼-아이콘화.md @@ -125,12 +125,30 @@ - [x] 커스텀 아이콘 삭제 시 디폴트 아이콘으로 복귀 → 아이콘 모드 유지 확인 - [x] deprecated 액션에서 디폴트 폴백 아이콘(SquareMousePointer) 표시 확인 -### 6단계: 정리 +### 6단계: 동적 아이콘 로딩 (뷰어 렌더링 누락 수정) -- [x] TypeScript 컴파일 에러 없음 확인 (우리 파일 6개 모두 0 에러) +- [x] `button-icon-map.tsx`에 `icons as allLucideIcons` import 추가 +- [x] `getLucideIcon()` — `iconMap` miss 시 `allLucideIcons` fallback 조회 + 캐싱 +- [x] `allLucideIcons`를 `button-icon-map.tsx`에서 re-export (import 중앙화) +- [x] `ButtonConfigPanel.tsx` — `lucide-react` 직접 import 제거, `button-icon-map`에서 import로 통합 +- [x] 화면 관리에서 선택한 커스텀 lucide 아이콘이 실제 화면(뷰어)에서도 렌더링됨 확인 + +### 7단계: 정리 + +- [x] TypeScript 컴파일 에러 없음 확인 - [x] 불필요한 import 없음 확인 +- [x] 문서 3개 최신화 (동적 로딩 반영) - [x] 이 체크리스트 완료 표시 업데이트 +### 8단계: 커스텀 아이콘 전역 관리 (미구현) + +- [ ] `custom_icon_registry` 테이블 마이그레이션 SQL 작성 및 실행 (개발섭 + 본섭) +- [ ] 백엔드 API 구현 (GET/POST/DELETE `/api/custom-icons`) +- [ ] 프론트엔드 API 클라이언트 함수 추가 (`lib/api/`) +- [ ] `ButtonConfigPanel` — 커스텀 아이콘 조회/추가/삭제를 전역 API로 변경 +- [ ] 기존 `componentConfig.customIcons` 하위 호환 병합 처리 +- [ ] 검증: 화면 A에서 추가한 커스텀 아이콘이 화면 B에서도 보이는지 확인 + --- ## 변경 이력 @@ -156,3 +174,6 @@ | 2026-03-04 | 텍스트 위치 4방향 설정 추가 (왼쪽/오른쪽/위쪽/아래쪽) | | 2026-03-04 | 버튼 테두리 이중 적용 수정 — position wrapper에서 border strip, border shorthand 제거 | | 2026-03-04 | 프리셋 라벨 한글화 (작게/보통/크게/매우 크게), 라벨 "아이콘 크기 비율"로 변경 | +| 2026-03-13 | 동적 아이콘 로딩 — `getLucideIcon()` fallback으로 `allLucideIcons` 조회+캐싱, import 중앙화 | +| 2026-03-13 | 문서 3개 최신화 (계획서 설계 원칙, 맥락노트 결정사항 #18, 체크리스트 6-7단계) | +| 2026-03-13 | 커스텀 아이콘 전역 관리 계획 추가 (8단계, 미구현) — DB 테이블 + API + 프론트 변경 예정 | diff --git a/docs/ycshin-node/BTN-일괄변경-탑씰-버튼스타일.md b/docs/ycshin-node/BTN-일괄변경-탑씰-버튼스타일.md new file mode 100644 index 00000000..83976b73 --- /dev/null +++ b/docs/ycshin-node/BTN-일괄변경-탑씰-버튼스타일.md @@ -0,0 +1,171 @@ +# BTN - 버튼 UI 스타일 기준정보 + +## 1. 스타일 기준 + +### 공통 스타일 + +| 항목 | 값 | +|---|---| +| 높이 | 40px | +| 표시모드 | 아이콘 + 텍스트 (icon-text) | +| 아이콘 | 액션별 첫 번째 기본 아이콘 (자동 선택) | +| 아이콘 크기 비율 | 보통 | +| 아이콘-텍스트 간격 | 6px | +| 텍스트 위치 | 오른쪽 (아이콘 왼쪽, 텍스트 오른쪽) | +| 테두리 모서리 | 8px | +| 테두리 색상/두께 | 없음 (투명, borderWidth: 0) | +| 텍스트 색상 | #FFFFFF (흰색) | +| 텍스트 크기 | 12px | +| 텍스트 굵기 | normal (보통) | +| 텍스트 정렬 | 왼쪽 | + +### 배경색 (액션별) + +| 액션 타입 | 배경색 | 비고 | +|---|---|---| +| `delete` | `#F04544` | 빨간색 | +| `excel_download`, `excel_upload`, `multi_table_excel_upload` | `#212121` | 검정색 | +| 그 외 모든 액션 | `#3B83F6` | 파란색 (기본값) | + +배경색은 디자이너에서 액션을 변경하면 자동으로 바뀐다. + +### 너비 (텍스트 글자수별) + +| 글자수 | 너비 | +|---|---| +| 6글자 이하 | 140px | +| 7글자 이상 | 160px | + +### 액션별 기본 아이콘 + +디자이너에서 표시모드를 "아이콘" 또는 "아이콘+텍스트"로 변경하면 액션에 맞는 첫 번째 아이콘이 자동 선택된다. + +소스: `frontend/lib/button-icon-map.tsx` > `actionIconMap` + +| action.type | 기본 아이콘 | +|---|---| +| `save` | Check | +| `delete` | Trash2 | +| `edit` | Pencil | +| `navigate` | ArrowRight | +| `modal` | Maximize2 | +| `transferData` | SendHorizontal | +| `excel_download` | Download | +| `excel_upload` | Upload | +| `quickInsert` | Zap | +| `control` | Settings | +| `barcode_scan` | ScanLine | +| `operation_control` | Truck | +| `event` | Send | +| `copy` | Copy | +| (그 외/없음) | SquareMousePointer | + +--- + +## 2. 코드 반영 현황 + +### 컴포넌트 기본값 (신규 버튼 생성 시 적용) + +| 파일 | 내용 | +|---|---| +| `frontend/lib/registry/components/v2-button-primary/index.ts` | defaultConfig, defaultSize (140x40) | +| `frontend/lib/registry/components/v2-button-primary/config.ts` | ButtonPrimaryDefaultConfig | + +### 액션 변경 시 배경색 자동 변경 + +| 파일 | 내용 | +|---|---| +| `frontend/components/screen/config-panels/ButtonConfigPanel.tsx` | 액션 변경 시 배경색/텍스트색 자동 설정 | + +### 렌더링 배경색 우선순위 + +| 파일 | 내용 | +|---|---| +| `frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx` | 배경색 결정 우선순위 개선 | + +배경색 결정 순서: +1. `webTypeConfig.backgroundColor` +2. `componentConfig.backgroundColor` +3. `component.style.backgroundColor` +4. `componentConfig.style.backgroundColor` +5. `component.style.labelColor` (레거시 호환) +6. 액션별 기본 배경색 (`#F04544` / `#212121` / `#3B83F6`) + +### 미반영 (추후 작업) + +- split-panel 내부 버튼의 코드 기본값 (split-panel 컴포넌트가 자체 생성하는 버튼) + +--- + +## 3. DB 데이터 매핑 (layout_data JSON) + +버튼은 `layout_data.components[]` 배열 안에 `url`이 `v2-button-primary`인 컴포넌트로 저장된다. + +| 항목 | JSON 위치 | 값 | +|---|---|---| +| 높이 | `size.height` | `40` | +| 너비 | `size.width` | `140` 또는 `160` | +| 표시모드 | `overrides.displayMode` | `"icon-text"` | +| 아이콘 이름 | `overrides.icon.name` | 액션별 영문 이름 | +| 아이콘 타입 | `overrides.icon.type` | `"lucide"` | +| 아이콘 크기 | `overrides.icon.size` | `"보통"` | +| 텍스트 위치 | `overrides.iconTextPosition` | `"right"` | +| 아이콘-텍스트 간격 | `overrides.iconGap` | `6` | +| 테두리 모서리 | `overrides.style.borderRadius` | `"8px"` | +| 텍스트 색상 | `overrides.style.labelColor` | `"#FFFFFF"` | +| 텍스트 크기 | `overrides.style.fontSize` | `"12px"` | +| 텍스트 굵기 | `overrides.style.fontWeight` | `"normal"` | +| 텍스트 정렬 | `overrides.style.labelTextAlign` | `"left"` | +| 배경색 | `overrides.style.backgroundColor` | 액션별 색상 | + +버튼이 위치하는 구조별 경로: +- 일반 버튼: `layout_data.components[]` +- 탭 위젯 내부: `layout_data.components[].overrides.tabs[].components[]` +- split-panel 내부: `layout_data.components[].overrides.rightPanel.components[]` + +--- + +## 4. 탑씰(COMPANY_7) 일괄 변경 작업 기록 + +### 대상 +- **회사**: 탑씰 (company_code = 'COMPANY_7') +- **테이블**: screen_layouts_v2 (배포서버) +- **스크립트**: `backend-node/scripts/btn-bulk-update-company7.ts` +- **백업 테이블**: `screen_layouts_v2_backup_company7` + +### 작업 이력 + +| 날짜 | 작업 내용 | 비고 | +|---|---|---| +| 2026-03-13 | 백업 테이블 생성 | | +| 2026-03-13 | 전체 버튼 공통 스타일 일괄 적용 | 높이, 아이콘, 텍스트 스타일, 배경색, 모서리 | +| 2026-03-13 | 탭 위젯 내부 버튼 스타일 보정 | componentConfig + root style 양쪽 적용 | +| 2026-03-13 | fontWeight "400" → "normal" 보정 | | +| 2026-03-13 | overrides.style.width 제거 | size.width와 충돌 방지 | +| 2026-03-13 | save 액션 55개에 "저장" 텍스트 명시 | | +| 2026-03-13 | "엑셀다운로드" → "Excel" 텍스트 통일 | | +| 2026-03-13 | Excel 버튼 배경색 #212121 통일 | | +| 2026-03-13 | 전체 버튼 너비 140px 통일 | | +| 2026-03-13 | 7글자 이상 버튼 너비 160px 재조정 | | +| 2026-03-13 | split-panel 내부 버튼 스타일 적용 | BOM관리 등 7개 버튼 | + +### 스킵 항목 +- `transferData` 액션의 텍스트 없는 버튼 1개 (screen=5976) + +### 알려진 이슈 +- **반응형 너비 불일치**: 디자이너에서 설정한 `size.width`가 실제 화면(`ResponsiveGridRenderer`)에서 반영되지 않을 수 있음. 버튼 wrapper에 `width` 속성이 누락되어 flex shrink-to-fit 동작으로 너비가 줄어드는 현상. 세로(height)는 정상 반영됨. + +### 원복 (필요 시) + +```sql +UPDATE screen_layouts_v2 AS target +SET layout_data = backup.layout_data +FROM screen_layouts_v2_backup_company7 AS backup +WHERE target.layout_id = backup.layout_id; +``` + +### 백업 테이블 정리 + +```sql +DROP TABLE screen_layouts_v2_backup_company7; +``` diff --git a/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md index b74eed58..cbcb5f27 100644 --- a/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md +++ b/docs/ycshin-node/MPN[체크]-품번-수동접두어채번.md @@ -6,8 +6,8 @@ ## 공정 상태 -- 전체 진행률: **95%** (코드 구현 + DB 마이그레이션 + 실시간 미리보기 + 코드 정리 완료, 검증 대기) -- 현재 단계: 검증 대기 +- 전체 진행률: **100%** (전체 완료) +- 현재 단계: 완료 --- @@ -45,13 +45,13 @@ ### 6단계: 검증 -- [ ] 카테고리 선택 + 수동입력 "ㅁㅁㅁ" → 카테고리값-ㅁㅁㅁ-001 생성 확인 -- [ ] 카테고리 미선택 + 수동입력 "ㅁㅁㅁ" → -ㅁㅁㅁ-001 생성 확인 (-- 아님) -- [ ] 같은 접두어 "ㅁㅁㅁ" 재등록 → -ㅁㅁㅁ-002 순번 증가 확인 -- [ ] 다른 접두어 "ㅇㅇㅇ" 등록 → -ㅇㅇㅇ-001 독립 시퀀스 확인 -- [ ] 수동 파트 없는 채번 규칙 동작 영향 없음 확인 -- [ ] previewCode (미리보기) 동작 영향 없음 확인 -- [ ] BULK1이 더 이상 생성되지 않음 확인 +- [x] 카테고리 선택 + 수동입력 "ㅁㅁㅁ" → 카테고리값-ㅁㅁㅁ-001 생성 확인 +- [x] 카테고리 미선택 + 수동입력 "ㅁㅁㅁ" → -ㅁㅁㅁ-001 생성 확인 (-- 아님) +- [x] 같은 접두어 "ㅁㅁㅁ" 재등록 → -ㅁㅁㅁ-002 순번 증가 확인 +- [x] 다른 접두어 "ㅇㅇㅇ" 등록 → -ㅇㅇㅇ-001 독립 시퀀스 확인 +- [x] 수동 파트 없는 채번 규칙 동작 영향 없음 확인 +- [x] previewCode (미리보기) 동작 영향 없음 확인 +- [x] BULK1이 더 이상 생성되지 않음 확인 ### 7단계: 실시간 순번 미리보기 @@ -97,3 +97,4 @@ | 2026-03-12 | 초기 상태 레거시 시퀀스 조회 방지 수정 + 계맥체 반영 | | 2026-03-12 | 카테고리 변경 시 수동 입력값 포함 순번 재조회 수정 | | 2026-03-12 | resolveCategoryFormat 헬퍼 추출 코드 정리 + 계맥체 최신화 | +| 2026-03-12 | 6단계 검증 완료. 전체 완료 | diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 295371c0..14e123ed 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -33,7 +33,6 @@ import { QuickInsertConfigSection } from "./QuickInsertConfigSection"; import { getApprovalDefinitions, type ApprovalDefinition } from "@/lib/api/approval"; import DOMPurify from "isomorphic-dompurify"; import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent"; -import { icons as allLucideIcons } from "lucide-react"; import { actionIconMap, noIconActions, @@ -42,6 +41,7 @@ import { getLucideIcon, addToIconMap, getDefaultIconForAction, + allLucideIcons, } from "@/lib/button-icon-map"; // 🆕 제목 블록 타입 @@ -989,8 +989,15 @@ export const ButtonConfigPanel: React.FC = ({ } setTimeout(() => { - const newColor = value === "delete" ? "#ef4444" : "#212121"; - onUpdateProperty("style.labelColor", newColor); + const excelActions = ["excel_download", "excel_upload", "multi_table_excel_upload"]; + let newBgColor = "#3B83F6"; + if (value === "delete") { + newBgColor = "#F04544"; + } else if (excelActions.includes(value)) { + newBgColor = "#212121"; + } + onUpdateProperty("style.backgroundColor", newBgColor); + onUpdateProperty("style.labelColor", "#FFFFFF"); }, 100); }} > diff --git a/frontend/lib/button-icon-map.tsx b/frontend/lib/button-icon-map.tsx index d8c38b25..03b204b6 100644 --- a/frontend/lib/button-icon-map.tsx +++ b/frontend/lib/button-icon-map.tsx @@ -16,11 +16,12 @@ import { Send, Radio, Megaphone, Podcast, BellRing, Copy, ClipboardCopy, Files, CopyPlus, ClipboardList, Clipboard, SquareMousePointer, + icons as allLucideIcons, type LucideIcon, } from "lucide-react"; // --------------------------------------------------------------------------- -// 아이콘 이름 → 컴포넌트 매핑 (추천 아이콘만 명시적 import) +// 아이콘 이름 → 컴포넌트 매핑 (추천 아이콘은 명시적 import, 나머지는 동적 조회) // --------------------------------------------------------------------------- export const iconMap: Record = { Check, Save, CheckCircle, CircleCheck, FileCheck, ShieldCheck, @@ -106,15 +107,27 @@ export function getIconSizeStyle(size: string | number): React.CSSProperties { // --------------------------------------------------------------------------- // 아이콘 조회 / 동적 등록 +// iconMap에 없으면 lucide-react 전체 아이콘에서 동적 조회 후 캐싱 // --------------------------------------------------------------------------- export function getLucideIcon(name: string): LucideIcon | undefined { - return iconMap[name]; + if (iconMap[name]) return iconMap[name]; + + const found = allLucideIcons[name as keyof typeof allLucideIcons]; + if (found) { + iconMap[name] = found; + return found; + } + + return undefined; } export function addToIconMap(name: string, component: LucideIcon): void { iconMap[name] = component; } +// ButtonConfigPanel 등에서 전체 아이콘 검색용으로 사용 +export { allLucideIcons }; + // --------------------------------------------------------------------------- // SVG 정화 // --------------------------------------------------------------------------- diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index fa0cfaae..4d89f80b 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -502,15 +502,22 @@ export const ButtonPrimaryComponent: React.FC = ({ if (component.style?.backgroundColor) { return component.style.backgroundColor; } - // 4순위: style.labelColor (레거시) + // 4순위: componentConfig.style.backgroundColor + if (componentConfig.style?.backgroundColor) { + return componentConfig.style.backgroundColor; + } + // 5순위: style.labelColor (레거시 호환) if (component.style?.labelColor) { return component.style.labelColor; } - // 기본값: 삭제 버튼이면 빨강, 아니면 파랑 - if (isDeleteAction()) { - return "#ef4444"; // 빨간색 (Tailwind red-500) - } - return "#3b82f6"; // 파란색 (Tailwind blue-500) + // 6순위: 액션별 기본 배경색 + const excelActions = ["excel_download", "excel_upload", "multi_table_excel_upload"]; + const actionType = typeof componentConfig.action === "string" + ? componentConfig.action + : componentConfig.action?.type || ""; + if (actionType === "delete") return "#F04544"; + if (excelActions.includes(actionType)) return "#212121"; + return "#3B83F6"; }; const getButtonTextColor = () => { diff --git a/frontend/lib/registry/components/v2-button-primary/config.ts b/frontend/lib/registry/components/v2-button-primary/config.ts index 06f73556..66ff9173 100644 --- a/frontend/lib/registry/components/v2-button-primary/config.ts +++ b/frontend/lib/registry/components/v2-button-primary/config.ts @@ -6,16 +6,29 @@ import { ButtonPrimaryConfig } from "./types"; * ButtonPrimary 컴포넌트 기본 설정 */ export const ButtonPrimaryDefaultConfig: ButtonPrimaryConfig = { - text: "버튼", + text: "저장", actionType: "button", - variant: "primary", - - // 공통 기본값 + variant: "default", + size: "md", disabled: false, required: false, readonly: false, - variant: "default", - size: "md", + displayMode: "icon-text", + icon: { + name: "Check", + type: "lucide", + size: "보통", + }, + iconTextPosition: "right", + iconGap: 6, + style: { + borderRadius: "8px", + labelColor: "#FFFFFF", + fontSize: "12px", + fontWeight: "normal", + labelTextAlign: "left", + backgroundColor: "#3B83F6", + }, }; /** diff --git a/frontend/lib/registry/components/v2-button-primary/index.ts b/frontend/lib/registry/components/v2-button-primary/index.ts index 57e57d34..5bf5e193 100644 --- a/frontend/lib/registry/components/v2-button-primary/index.ts +++ b/frontend/lib/registry/components/v2-button-primary/index.ts @@ -28,8 +28,24 @@ export const V2ButtonPrimaryDefinition = createComponentDefinition({ successMessage: "저장되었습니다.", errorMessage: "저장 중 오류가 발생했습니다.", }, + displayMode: "icon-text", + icon: { + name: "Check", + type: "lucide", + size: "보통", + }, + iconTextPosition: "right", + iconGap: 6, + style: { + borderRadius: "8px", + labelColor: "#FFFFFF", + fontSize: "12px", + fontWeight: "normal", + labelTextAlign: "left", + backgroundColor: "#3B83F6", + }, }, - defaultSize: { width: 120, height: 40 }, + defaultSize: { width: 140, height: 40 }, configPanel: undefined, // 상세 설정 패널(ButtonConfigPanel)이 대신 사용됨 icon: "MousePointer", tags: ["버튼", "액션", "클릭"], From 6505df855555f1ecf3d299a1954ed2ea7604342d Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 16 Mar 2026 10:40:10 +0900 Subject: [PATCH 15/18] feat: enhance v2-timeline-scheduler component functionality - Updated the v2-timeline-scheduler documentation to reflect the latest implementation status and enhancements. - Improved the TimelineSchedulerComponent by integrating conflict detection and milestone rendering features. - Refactored ResourceRow and ScheduleBar components to support new props for handling conflicts and milestones. - Added visual indicators for conflicts and milestones to enhance user experience and clarity in scheduling. These changes aim to improve the functionality and usability of the timeline scheduler within the ERP system. Made-with: Cursor --- .../next-component-development-plan.md | 14 +- .../TimelineSchedulerComponent.tsx | 293 +++++++++++------- .../components/ResourceRow.tsx | 98 +++--- .../components/ScheduleBar.tsx | 198 ++++++++---- .../components/TimelineLegend.tsx | 55 ++++ .../v2-timeline-scheduler/components/index.ts | 1 + .../utils/conflictDetection.ts | 58 ++++ 7 files changed, 493 insertions(+), 224 deletions(-) create mode 100644 frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineLegend.tsx create mode 100644 frontend/lib/registry/components/v2-timeline-scheduler/utils/conflictDetection.ts diff --git a/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md index 58c8cd3f..84f6c789 100644 --- a/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md +++ b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md @@ -531,7 +531,7 @@ function detectConflicts(schedules: ScheduleItem[], resourceId: string): Schedul - [x] 레지스트리 등록 - [x] 문서화 (README.md) -#### v2-timeline-scheduler ✅ 구현 완료 (2026-01-30) +#### v2-timeline-scheduler ✅ 구현 완료 (2026-01-30, 업데이트: 2026-03-13) - [x] 타입 정의 완료 - [x] 기본 구조 생성 @@ -539,12 +539,16 @@ function detectConflicts(schedules: ScheduleItem[], resourceId: string): Schedul - [x] TimelineGrid (배경) - [x] ResourceColumn (리소스) - [x] ScheduleBar 기본 렌더링 -- [x] 드래그 이동 (기본) -- [x] 리사이즈 (기본) +- [x] 드래그 이동 (실제 로직: deltaX → 날짜 계산 → API 저장 → toast) +- [x] 리사이즈 (실제 로직: 시작/종료 핸들 → 기간 변경 → API 저장 → toast) - [x] 줌 레벨 전환 - [x] 날짜 네비게이션 -- [ ] 충돌 감지 (향후) -- [ ] 가상 스크롤 (향후) +- [x] 충돌 감지 (같은 리소스 겹침 → ring-destructive + AlertTriangle) +- [x] 마일스톤 표시 (시작일 = 종료일 → 다이아몬드 마커) +- [x] 범례 표시 (TimelineLegend: 상태별 색상 + 마일스톤 + 충돌) +- [x] 반응형 공통 CSS 적용 (text-[10px] sm:text-sm 패턴) +- [x] staticFilters 지원 (커스텀 테이블 필터링) +- [ ] 가상 스크롤 (향후 - 대용량 100+ 리소스) - [x] 설정 패널 구현 - [x] API 연동 - [x] 레지스트리 등록 diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx index e7da45a6..354869bc 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useMemo, useRef, useState } from "react"; +import React, { useCallback, useMemo, useRef } from "react"; import { ChevronLeft, ChevronRight, @@ -11,17 +11,16 @@ import { ZoomOut, } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { toast } from "sonner"; import { TimelineSchedulerComponentProps, ScheduleItem, ZoomLevel, - DragEvent, - ResizeEvent, } from "./types"; import { useTimelineData } from "./hooks/useTimelineData"; -import { TimelineHeader, ResourceRow } from "./components"; +import { TimelineHeader, ResourceRow, TimelineLegend } from "./components"; import { zoomLevelOptions, defaultTimelineSchedulerConfig } from "./config"; +import { detectConflicts, addDaysToDateString } from "./utils/conflictDetection"; /** * v2-timeline-scheduler 메인 컴포넌트 @@ -45,19 +44,6 @@ export function TimelineSchedulerComponent({ }: TimelineSchedulerComponentProps) { const containerRef = useRef(null); - // 드래그/리사이즈 상태 - const [dragState, setDragState] = useState<{ - schedule: ScheduleItem; - startX: number; - startY: number; - } | null>(null); - - const [resizeState, setResizeState] = useState<{ - schedule: ScheduleItem; - direction: "start" | "end"; - startX: number; - } | null>(null); - // 타임라인 데이터 훅 const { schedules, @@ -78,53 +64,43 @@ export function TimelineSchedulerComponent({ const error = externalError ?? hookError; // 설정값 - const rowHeight = config.rowHeight || defaultTimelineSchedulerConfig.rowHeight!; - const headerHeight = config.headerHeight || defaultTimelineSchedulerConfig.headerHeight!; + const rowHeight = + config.rowHeight || defaultTimelineSchedulerConfig.rowHeight!; + const headerHeight = + config.headerHeight || defaultTimelineSchedulerConfig.headerHeight!; const resourceColumnWidth = - config.resourceColumnWidth || defaultTimelineSchedulerConfig.resourceColumnWidth!; - const cellWidthConfig = config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!; + config.resourceColumnWidth || + defaultTimelineSchedulerConfig.resourceColumnWidth!; + const cellWidthConfig = + config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!; const cellWidth = cellWidthConfig[zoomLevel] || 60; - // 리소스가 없으면 스케줄의 resourceId로 자동 생성 + // 리소스 자동 생성 (리소스 테이블 미설정 시 스케줄 데이터에서 추출) const effectiveResources = useMemo(() => { - if (resources.length > 0) { - return resources; - } + if (resources.length > 0) return resources; - // 스케줄에서 고유한 resourceId 추출하여 자동 리소스 생성 const uniqueResourceIds = new Set(); - schedules.forEach((schedule) => { - if (schedule.resourceId) { - uniqueResourceIds.add(schedule.resourceId); - } + schedules.forEach((s) => { + if (s.resourceId) uniqueResourceIds.add(s.resourceId); }); - return Array.from(uniqueResourceIds).map((id) => ({ - id, - name: id, // resourceId를 이름으로 사용 - })); + return Array.from(uniqueResourceIds).map((id) => ({ id, name: id })); }, [resources, schedules]); // 리소스별 스케줄 그룹화 const schedulesByResource = useMemo(() => { const grouped = new Map(); - effectiveResources.forEach((resource) => { - grouped.set(resource.id, []); - }); + effectiveResources.forEach((r) => grouped.set(r.id, [])); schedules.forEach((schedule) => { const list = grouped.get(schedule.resourceId); if (list) { list.push(schedule); } else { - // 리소스가 없는 스케줄은 첫 번째 리소스에 할당 const firstResource = effectiveResources[0]; if (firstResource) { - const firstList = grouped.get(firstResource.id); - if (firstList) { - firstList.push(schedule); - } + grouped.get(firstResource.id)?.push(schedule); } } }); @@ -132,27 +108,31 @@ export function TimelineSchedulerComponent({ return grouped; }, [schedules, effectiveResources]); - // 줌 레벨 변경 + // ────────── 충돌 감지 ────────── + const conflictIds = useMemo(() => { + if (config.showConflicts === false) return new Set(); + return detectConflicts(schedules); + }, [schedules, config.showConflicts]); + + // ────────── 줌 레벨 변경 ────────── const handleZoomIn = useCallback(() => { const levels: ZoomLevel[] = ["month", "week", "day"]; - const currentIdx = levels.indexOf(zoomLevel); - if (currentIdx < levels.length - 1) { - setZoomLevel(levels[currentIdx + 1]); - } + const idx = levels.indexOf(zoomLevel); + if (idx < levels.length - 1) setZoomLevel(levels[idx + 1]); }, [zoomLevel, setZoomLevel]); const handleZoomOut = useCallback(() => { const levels: ZoomLevel[] = ["month", "week", "day"]; - const currentIdx = levels.indexOf(zoomLevel); - if (currentIdx > 0) { - setZoomLevel(levels[currentIdx - 1]); - } + const idx = levels.indexOf(zoomLevel); + if (idx > 0) setZoomLevel(levels[idx - 1]); }, [zoomLevel, setZoomLevel]); - // 스케줄 클릭 핸들러 + // ────────── 스케줄 클릭 ────────── const handleScheduleClick = useCallback( (schedule: ScheduleItem) => { - const resource = effectiveResources.find((r) => r.id === schedule.resourceId); + const resource = effectiveResources.find( + (r) => r.id === schedule.resourceId + ); if (resource && onScheduleClick) { onScheduleClick({ schedule, resource }); } @@ -160,7 +140,7 @@ export function TimelineSchedulerComponent({ [effectiveResources, onScheduleClick] ); - // 빈 셀 클릭 핸들러 + // ────────── 빈 셀 클릭 ────────── const handleCellClick = useCallback( (resourceId: string, date: Date) => { if (onCellClick) { @@ -173,47 +153,111 @@ export function TimelineSchedulerComponent({ [onCellClick] ); - // 드래그 시작 - const handleDragStart = useCallback( - (schedule: ScheduleItem, e: React.MouseEvent) => { - setDragState({ - schedule, - startX: e.clientX, - startY: e.clientY, - }); + // ────────── 드래그 완료 (핵심 로직) ────────── + const handleDragComplete = useCallback( + async (schedule: ScheduleItem, deltaX: number) => { + // 줌 레벨에 따라 1셀당 일수가 달라짐 + let daysPerCell = 1; + if (zoomLevel === "week") daysPerCell = 7; + if (zoomLevel === "month") daysPerCell = 30; + + const deltaDays = Math.round((deltaX / cellWidth) * daysPerCell); + if (deltaDays === 0) return; + + const newStartDate = addDaysToDateString(schedule.startDate, deltaDays); + const newEndDate = addDaysToDateString(schedule.endDate, deltaDays); + + try { + await updateSchedule(schedule.id, { + startDate: newStartDate, + endDate: newEndDate, + }); + + // 외부 이벤트 핸들러 호출 + onDragEnd?.({ + scheduleId: schedule.id, + newStartDate, + newEndDate, + }); + + toast.success("스케줄 이동 완료", { + description: `${schedule.title}: ${newStartDate} ~ ${newEndDate}`, + }); + } catch (err: any) { + toast.error("스케줄 이동 실패", { + description: err.message || "잠시 후 다시 시도해주세요", + }); + } }, - [] + [cellWidth, zoomLevel, updateSchedule, onDragEnd] ); - // 드래그 종료 - const handleDragEnd = useCallback(() => { - if (dragState) { - // TODO: 드래그 결과 계산 및 업데이트 - setDragState(null); - } - }, [dragState]); + // ────────── 리사이즈 완료 (핵심 로직) ────────── + const handleResizeComplete = useCallback( + async ( + schedule: ScheduleItem, + direction: "start" | "end", + deltaX: number + ) => { + let daysPerCell = 1; + if (zoomLevel === "week") daysPerCell = 7; + if (zoomLevel === "month") daysPerCell = 30; - // 리사이즈 시작 - const handleResizeStart = useCallback( - (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => { - setResizeState({ - schedule, - direction, - startX: e.clientX, - }); + const deltaDays = Math.round((deltaX / cellWidth) * daysPerCell); + if (deltaDays === 0) return; + + let newStartDate = schedule.startDate; + let newEndDate = schedule.endDate; + + if (direction === "start") { + newStartDate = addDaysToDateString(schedule.startDate, deltaDays); + // 시작일이 종료일을 넘지 않도록 + if (new Date(newStartDate) >= new Date(newEndDate)) { + toast.warning("시작일은 종료일보다 이전이어야 합니다"); + return; + } + } else { + newEndDate = addDaysToDateString(schedule.endDate, deltaDays); + // 종료일이 시작일보다 앞서지 않도록 + if (new Date(newEndDate) <= new Date(newStartDate)) { + toast.warning("종료일은 시작일보다 이후여야 합니다"); + return; + } + } + + try { + await updateSchedule(schedule.id, { + startDate: newStartDate, + endDate: newEndDate, + }); + + onResizeEnd?.({ + scheduleId: schedule.id, + newStartDate, + newEndDate, + direction, + }); + + const days = + Math.round( + (new Date(newEndDate).getTime() - + new Date(newStartDate).getTime()) / + (1000 * 60 * 60 * 24) + ) + 1; + + toast.success("기간 변경 완료", { + description: `${schedule.title}: ${days}일 (${newStartDate} ~ ${newEndDate})`, + }); + } catch (err: any) { + toast.error("기간 변경 실패", { + description: err.message || "잠시 후 다시 시도해주세요", + }); + } }, - [] + [cellWidth, zoomLevel, updateSchedule, onResizeEnd] ); - // 리사이즈 종료 - const handleResizeEnd = useCallback(() => { - if (resizeState) { - // TODO: 리사이즈 결과 계산 및 업데이트 - setResizeState(null); - } - }, [resizeState]); - - // 추가 버튼 클릭 + // ────────── 추가 버튼 클릭 ────────── const handleAddClick = useCallback(() => { if (onAddSchedule && effectiveResources.length > 0) { onAddSchedule( @@ -223,7 +267,13 @@ export function TimelineSchedulerComponent({ } }, [onAddSchedule, effectiveResources]); - // 디자인 모드 플레이스홀더 + // ────────── 하단 영역 높이 계산 (툴바 + 범례) ────────── + const showToolbar = config.showToolbar !== false; + const showLegend = config.showLegend !== false; + const toolbarHeight = showToolbar ? 36 : 0; + const legendHeight = showLegend ? 28 : 0; + + // ────────── 디자인 모드 플레이스홀더 ────────── if (isDesignMode) { return (
@@ -240,7 +290,7 @@ export function TimelineSchedulerComponent({ ); } - // 로딩 상태 + // ────────── 로딩 상태 ────────── if (isLoading) { return (
-

스케줄 데이터가 없습니다

+

+ 스케줄 데이터가 없습니다 +

- 좌측 테이블에서 품목을 선택하거나,
+ 좌측 테이블에서 품목을 선택하거나, +
스케줄 생성 버튼을 눌러 스케줄을 생성하세요

@@ -289,18 +342,19 @@ export function TimelineSchedulerComponent({ ); } + // ────────── 메인 렌더링 ────────── return (
{/* 툴바 */} - {config.showToolbar !== false && ( -
+ {showToolbar && ( +
{/* 네비게이션 */}
{config.showNavigation !== false && ( @@ -332,16 +386,23 @@ export function TimelineSchedulerComponent({ )} - {/* 현재 날짜 범위 표시 */} + {/* 날짜 범위 표시 */} {viewStartDate.getFullYear()}년 {viewStartDate.getMonth() + 1}월{" "} - {viewStartDate.getDate()}일 ~{" "} - {viewEndDate.getMonth() + 1}월 {viewEndDate.getDate()}일 + {viewStartDate.getDate()}일 ~ {viewEndDate.getMonth() + 1}월{" "} + {viewEndDate.getDate()}일
{/* 오른쪽 컨트롤 */}
+ {/* 충돌 카운트 표시 */} + {config.showConflicts !== false && conflictIds.size > 0 && ( + + 충돌 {conflictIds.size}건 + + )} + {/* 줌 컨트롤 */} {config.showZoomControls !== false && (
@@ -355,7 +416,10 @@ export function TimelineSchedulerComponent({ - {zoomLevelOptions.find((o) => o.value === zoomLevel)?.label} + { + zoomLevelOptions.find((o) => o.value === zoomLevel) + ?.label + }
); } diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx index 75a465a3..4e248cd6 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ResourceRow.tsx @@ -2,54 +2,44 @@ import React, { useMemo } from "react"; import { cn } from "@/lib/utils"; -import { Resource, ScheduleItem, ZoomLevel, TimelineSchedulerConfig } from "../types"; +import { + Resource, + ScheduleItem, + ZoomLevel, + TimelineSchedulerConfig, +} from "../types"; import { ScheduleBar } from "./ScheduleBar"; interface ResourceRowProps { - /** 리소스 */ resource: Resource; - /** 해당 리소스의 스케줄 목록 */ schedules: ScheduleItem[]; - /** 시작 날짜 */ startDate: Date; - /** 종료 날짜 */ endDate: Date; - /** 줌 레벨 */ zoomLevel: ZoomLevel; - /** 행 높이 */ rowHeight: number; - /** 셀 너비 */ cellWidth: number; - /** 리소스 컬럼 너비 */ resourceColumnWidth: number; - /** 설정 */ config: TimelineSchedulerConfig; - /** 스케줄 클릭 */ + /** 충돌 스케줄 ID 목록 */ + conflictIds?: Set; onScheduleClick?: (schedule: ScheduleItem) => void; - /** 빈 셀 클릭 */ onCellClick?: (resourceId: string, date: Date) => void; - /** 드래그 시작 */ - onDragStart?: (schedule: ScheduleItem, e: React.MouseEvent) => void; - /** 드래그 종료 */ - onDragEnd?: () => void; - /** 리사이즈 시작 */ - onResizeStart?: (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => void; - /** 리사이즈 종료 */ - onResizeEnd?: () => void; + /** 드래그 완료: deltaX(픽셀) 전달 */ + onDragComplete?: (schedule: ScheduleItem, deltaX: number) => void; + /** 리사이즈 완료: direction + deltaX(픽셀) 전달 */ + onResizeComplete?: ( + schedule: ScheduleItem, + direction: "start" | "end", + deltaX: number + ) => void; } -/** - * 날짜 차이 계산 (일수) - */ const getDaysDiff = (start: Date, end: Date): number => { const startTime = new Date(start).setHours(0, 0, 0, 0); const endTime = new Date(end).setHours(0, 0, 0, 0); return Math.round((endTime - startTime) / (1000 * 60 * 60 * 24)); }; -/** - * 날짜 범위 내의 셀 개수 계산 - */ const getCellCount = (startDate: Date, endDate: Date): number => { return getDaysDiff(startDate, endDate) + 1; }; @@ -64,20 +54,18 @@ export function ResourceRow({ cellWidth, resourceColumnWidth, config, + conflictIds, onScheduleClick, onCellClick, - onDragStart, - onDragEnd, - onResizeStart, - onResizeEnd, + onDragComplete, + onResizeComplete, }: ResourceRowProps) { - // 총 셀 개수 - const totalCells = useMemo(() => getCellCount(startDate, endDate), [startDate, endDate]); - - // 총 그리드 너비 + const totalCells = useMemo( + () => getCellCount(startDate, endDate), + [startDate, endDate] + ); const gridWidth = totalCells * cellWidth; - // 오늘 날짜 const today = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); @@ -92,21 +80,26 @@ export function ResourceRow({ scheduleStart.setHours(0, 0, 0, 0); scheduleEnd.setHours(0, 0, 0, 0); - // 시작 위치 계산 const startOffset = getDaysDiff(startDate, scheduleStart); const left = Math.max(0, startOffset * cellWidth); - // 너비 계산 const durationDays = getDaysDiff(scheduleStart, scheduleEnd) + 1; const visibleStartOffset = Math.max(0, startOffset); const visibleEndOffset = Math.min( totalCells, startOffset + durationDays ); - const width = Math.max(cellWidth, (visibleEndOffset - visibleStartOffset) * cellWidth); + const width = Math.max( + cellWidth, + (visibleEndOffset - visibleStartOffset) * cellWidth + ); + + // 시작일 = 종료일이면 마일스톤 + const isMilestone = schedule.startDate === schedule.endDate; return { schedule, + isMilestone, position: { left: resourceColumnWidth + left, top: 0, @@ -115,9 +108,15 @@ export function ResourceRow({ }, }; }); - }, [schedules, startDate, cellWidth, resourceColumnWidth, rowHeight, totalCells]); + }, [ + schedules, + startDate, + cellWidth, + resourceColumnWidth, + rowHeight, + totalCells, + ]); - // 그리드 셀 클릭 핸들러 const handleGridClick = (e: React.MouseEvent) => { if (!onCellClick) return; @@ -142,7 +141,9 @@ export function ResourceRow({ style={{ width: resourceColumnWidth }} >
-
{resource.name}
+
+ {resource.name} +
{resource.group && (
{resource.group} @@ -162,7 +163,8 @@ export function ResourceRow({ {Array.from({ length: totalCells }).map((_, idx) => { const cellDate = new Date(startDate); cellDate.setDate(cellDate.getDate() + idx); - const isWeekend = cellDate.getDay() === 0 || cellDate.getDay() === 6; + const isWeekend = + cellDate.getDay() === 0 || cellDate.getDay() === 6; const isToday = cellDate.getTime() === today.getTime(); const isMonthStart = cellDate.getDate() === 1; @@ -182,22 +184,22 @@ export function ResourceRow({
{/* 스케줄 바들 */} - {schedulePositions.map(({ schedule, position }) => ( + {schedulePositions.map(({ schedule, position, isMilestone }) => ( onScheduleClick?.(schedule)} - onDragStart={onDragStart} - onDragEnd={onDragEnd} - onResizeStart={onResizeStart} - onResizeEnd={onResizeEnd} + onDragComplete={onDragComplete} + onResizeComplete={onResizeComplete} /> ))}
diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx index d547fafc..678dc3ac 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ScheduleBar.tsx @@ -2,79 +2,99 @@ import React, { useState, useCallback, useRef } from "react"; import { cn } from "@/lib/utils"; -import { ScheduleItem, ScheduleBarPosition, TimelineSchedulerConfig } from "../types"; +import { AlertTriangle } from "lucide-react"; +import { + ScheduleItem, + ScheduleBarPosition, + TimelineSchedulerConfig, +} from "../types"; import { statusOptions } from "../config"; interface ScheduleBarProps { - /** 스케줄 항목 */ schedule: ScheduleItem; - /** 위치 정보 */ position: ScheduleBarPosition; - /** 설정 */ config: TimelineSchedulerConfig; - /** 드래그 가능 여부 */ draggable?: boolean; - /** 리사이즈 가능 여부 */ resizable?: boolean; - /** 클릭 이벤트 */ + hasConflict?: boolean; + isMilestone?: boolean; onClick?: (schedule: ScheduleItem) => void; - /** 드래그 시작 */ - onDragStart?: (schedule: ScheduleItem, e: React.MouseEvent) => void; - /** 드래그 중 */ - onDrag?: (deltaX: number, deltaY: number) => void; - /** 드래그 종료 */ - onDragEnd?: () => void; - /** 리사이즈 시작 */ - onResizeStart?: (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => void; - /** 리사이즈 중 */ - onResize?: (deltaX: number, direction: "start" | "end") => void; - /** 리사이즈 종료 */ - onResizeEnd?: () => void; + /** 드래그 완료 시 deltaX(픽셀) 전달 */ + onDragComplete?: (schedule: ScheduleItem, deltaX: number) => void; + /** 리사이즈 완료 시 direction과 deltaX(픽셀) 전달 */ + onResizeComplete?: ( + schedule: ScheduleItem, + direction: "start" | "end", + deltaX: number + ) => void; } +// 드래그/리사이즈 판정 최소 이동 거리 (px) +const MIN_MOVE_THRESHOLD = 5; + export function ScheduleBar({ schedule, position, config, draggable = true, resizable = true, + hasConflict = false, + isMilestone = false, onClick, - onDragStart, - onDragEnd, - onResizeStart, - onResizeEnd, + onDragComplete, + onResizeComplete, }: ScheduleBarProps) { const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); + const [dragOffset, setDragOffset] = useState(0); + const [resizeOffset, setResizeOffset] = useState(0); + const [resizeDir, setResizeDir] = useState<"start" | "end">("end"); const barRef = useRef(null); + const startXRef = useRef(0); + const movedRef = useRef(false); - // 상태에 따른 색상 - const statusColor = schedule.color || + const statusColor = + schedule.color || config.statusColors?.[schedule.status] || statusOptions.find((s) => s.value === schedule.status)?.color || "#3b82f6"; - // 진행률 바 너비 - const progressWidth = config.showProgress && schedule.progress !== undefined - ? `${schedule.progress}%` - : "0%"; + const progressWidth = + config.showProgress && schedule.progress !== undefined + ? `${schedule.progress}%` + : "0%"; - // 드래그 시작 핸들러 + const isEditable = config.editable !== false; + + // ────────── 드래그 핸들러 ────────── const handleMouseDown = useCallback( (e: React.MouseEvent) => { - if (!draggable || isResizing) return; + if (!draggable || isResizing || !isEditable) return; e.preventDefault(); e.stopPropagation(); + + startXRef.current = e.clientX; + movedRef.current = false; setIsDragging(true); - onDragStart?.(schedule, e); + setDragOffset(0); const handleMouseMove = (moveEvent: MouseEvent) => { - // 드래그 중 로직은 부모에서 처리 + const delta = moveEvent.clientX - startXRef.current; + if (Math.abs(delta) > MIN_MOVE_THRESHOLD) { + movedRef.current = true; + } + setDragOffset(delta); }; - const handleMouseUp = () => { + const handleMouseUp = (upEvent: MouseEvent) => { + const finalDelta = upEvent.clientX - startXRef.current; setIsDragging(false); - onDragEnd?.(); + setDragOffset(0); + + if (movedRef.current && Math.abs(finalDelta) > MIN_MOVE_THRESHOLD) { + onDragComplete?.(schedule, finalDelta); + } + document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; @@ -82,25 +102,39 @@ export function ScheduleBar({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, - [draggable, isResizing, schedule, onDragStart, onDragEnd] + [draggable, isResizing, isEditable, schedule, onDragComplete] ); - // 리사이즈 시작 핸들러 - const handleResizeStart = useCallback( + // ────────── 리사이즈 핸들러 ────────── + const handleResizeMouseDown = useCallback( (direction: "start" | "end", e: React.MouseEvent) => { - if (!resizable) return; + if (!resizable || !isEditable) return; e.preventDefault(); e.stopPropagation(); + + startXRef.current = e.clientX; + movedRef.current = false; setIsResizing(true); - onResizeStart?.(schedule, direction, e); + setResizeOffset(0); + setResizeDir(direction); const handleMouseMove = (moveEvent: MouseEvent) => { - // 리사이즈 중 로직은 부모에서 처리 + const delta = moveEvent.clientX - startXRef.current; + if (Math.abs(delta) > MIN_MOVE_THRESHOLD) { + movedRef.current = true; + } + setResizeOffset(delta); }; - const handleMouseUp = () => { + const handleMouseUp = (upEvent: MouseEvent) => { + const finalDelta = upEvent.clientX - startXRef.current; setIsResizing(false); - onResizeEnd?.(); + setResizeOffset(0); + + if (movedRef.current && Math.abs(finalDelta) > MIN_MOVE_THRESHOLD) { + onResizeComplete?.(schedule, direction, finalDelta); + } + document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; @@ -108,19 +142,62 @@ export function ScheduleBar({ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, - [resizable, schedule, onResizeStart, onResizeEnd] + [resizable, isEditable, schedule, onResizeComplete] ); - // 클릭 핸들러 + // ────────── 클릭 핸들러 ────────── const handleClick = useCallback( (e: React.MouseEvent) => { - if (isDragging || isResizing) return; + if (movedRef.current) return; e.stopPropagation(); onClick?.(schedule); }, - [isDragging, isResizing, onClick, schedule] + [onClick, schedule] ); + // ────────── 드래그/리사이즈 중 시각적 위치 계산 ────────── + let visualLeft = position.left; + let visualWidth = position.width; + + if (isDragging) { + visualLeft += dragOffset; + } + + if (isResizing) { + if (resizeDir === "start") { + visualLeft += resizeOffset; + visualWidth -= resizeOffset; + } else { + visualWidth += resizeOffset; + } + } + + visualWidth = Math.max(10, visualWidth); + + // ────────── 마일스톤 렌더링 (단일 날짜 마커) ────────── + if (isMilestone) { + return ( +
+
+
+ ); + } + + // ────────── 일반 스케줄 바 렌더링 ────────── return (
{/* 진행률 바 */} {config.showProgress && schedule.progress !== undefined && ( @@ -162,19 +241,26 @@ export function ScheduleBar({
)} + {/* 충돌 인디케이터 */} + {hasConflict && ( +
+ +
+ )} + {/* 리사이즈 핸들 - 왼쪽 */} - {resizable && ( + {resizable && isEditable && (
handleResizeStart("start", e)} + onMouseDown={(e) => handleResizeMouseDown("start", e)} /> )} {/* 리사이즈 핸들 - 오른쪽 */} - {resizable && ( + {resizable && isEditable && (
handleResizeStart("end", e)} + onMouseDown={(e) => handleResizeMouseDown("end", e)} /> )}
diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineLegend.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineLegend.tsx new file mode 100644 index 00000000..da70e1b7 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/TimelineLegend.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React from "react"; +import { TimelineSchedulerConfig } from "../types"; +import { statusOptions } from "../config"; + +interface TimelineLegendProps { + config: TimelineSchedulerConfig; +} + +export function TimelineLegend({ config }: TimelineLegendProps) { + const colors = config.statusColors || {}; + + return ( +
+ + 범례: + + {statusOptions.map((status) => ( +
+
+ + {status.label} + +
+ ))} + + {/* 마일스톤 범례 */} +
+
+
+
+ + 마일스톤 + +
+ + {/* 충돌 범례 */} + {config.showConflicts && ( +
+
+ + 충돌 + +
+ )} +
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts index 4da03f17..4ac2af4b 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts @@ -1,3 +1,4 @@ export { TimelineHeader } from "./TimelineHeader"; export { ScheduleBar } from "./ScheduleBar"; export { ResourceRow } from "./ResourceRow"; +export { TimelineLegend } from "./TimelineLegend"; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/utils/conflictDetection.ts b/frontend/lib/registry/components/v2-timeline-scheduler/utils/conflictDetection.ts new file mode 100644 index 00000000..98b9fbb1 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/utils/conflictDetection.ts @@ -0,0 +1,58 @@ +"use client"; + +import { ScheduleItem } from "../types"; + +/** + * 같은 리소스에서 시간이 겹치는 스케줄을 감지 + * @returns 충돌이 있는 스케줄 ID Set + */ +export function detectConflicts(schedules: ScheduleItem[]): Set { + const conflictIds = new Set(); + + // 리소스별로 그룹화 + const byResource = new Map(); + for (const schedule of schedules) { + if (!byResource.has(schedule.resourceId)) { + byResource.set(schedule.resourceId, []); + } + byResource.get(schedule.resourceId)!.push(schedule); + } + + // 리소스별 충돌 검사 + for (const [, resourceSchedules] of byResource) { + if (resourceSchedules.length < 2) continue; + + // 시작일 기준 정렬 + const sorted = [...resourceSchedules].sort( + (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + + for (let i = 0; i < sorted.length; i++) { + const aEnd = new Date(sorted[i].endDate).getTime(); + + for (let j = i + 1; j < sorted.length; j++) { + const bStart = new Date(sorted[j].startDate).getTime(); + + // 정렬되어 있으므로 aStart <= bStart + // 겹치는 조건: aEnd > bStart + if (aEnd > bStart) { + conflictIds.add(sorted[i].id); + conflictIds.add(sorted[j].id); + } else { + break; + } + } + } + } + + return conflictIds; +} + +/** + * 날짜를 일수만큼 이동 + */ +export function addDaysToDateString(dateStr: string, days: number): string { + const date = new Date(dateStr); + date.setDate(date.getDate() + days); + return date.toISOString().split("T")[0]; +} From 7e02fff717055ee86e7e236bdd9cc9248f26e80e Mon Sep 17 00:00:00 2001 From: syc0123 Date: Mon, 16 Mar 2026 11:28:03 +0900 Subject: [PATCH 16/18] fix: update default button size in V2ButtonPrimary component - Changed the default width of the V2ButtonPrimary component from 140 to 100 pixels to improve UI consistency and responsiveness. - This adjustment aligns the button size with design specifications for better user experience. --- frontend/lib/registry/components/v2-button-primary/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/v2-button-primary/index.ts b/frontend/lib/registry/components/v2-button-primary/index.ts index 2e4c53eb..2aa4844c 100644 --- a/frontend/lib/registry/components/v2-button-primary/index.ts +++ b/frontend/lib/registry/components/v2-button-primary/index.ts @@ -43,7 +43,7 @@ export const V2ButtonPrimaryDefinition = createComponentDefinition({ backgroundColor: "#3B83F6", }, }, - defaultSize: { width: 140, height: 40 }, + defaultSize: { width: 100, height: 40 }, configPanel: V2ButtonConfigPanel, icon: "MousePointer", tags: ["버튼", "액션", "클릭"], From 64c9f25f630bdffc1a55dac13b76cf7f28dadc81 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 16 Mar 2026 14:00:07 +0900 Subject: [PATCH 17/18] feat: add schedule preview functionality for production plans - Implemented previewSchedule and previewSemiSchedule functions in the production controller to allow users to preview schedule changes without making actual database modifications. - Added corresponding routes for schedule preview in productionRoutes. - Enhanced productionPlanService with logic to generate schedule previews based on provided items and plan IDs. - Introduced SchedulePreviewDialog component to display the preview results in the frontend, including summary and detailed views of planned schedules. These updates improve the user experience by providing a way to visualize scheduling changes before applying them, ensuring better planning and decision-making. Made-with: Cursor --- .../src/controllers/productionController.ts | 42 ++ backend-node/src/routes/productionRoutes.ts | 6 + .../src/services/productionPlanService.ts | 281 +++++++-- .../next-component-development-plan.md | 4 +- .../schedule-auto-generation-guide.md | 76 +++ .../full-screen-analysis.md | 116 +++- .../v2-component-usage-guide.md | 375 ++++++++++- .../production-plan-test-scenario.md | 451 +++++++++++++ .../v2-table-grouped/hooks/useGroupedData.ts | 28 +- .../TimelineSchedulerComponent.tsx | 594 +++++++++++++++++- .../components/ItemTimelineCard.tsx | 297 +++++++++ .../components/SchedulePreviewDialog.tsx | 282 +++++++++ .../v2-timeline-scheduler/components/index.ts | 2 + .../components/v2-timeline-scheduler/types.ts | 29 + frontend/package-lock.json | 28 + frontend/package.json | 1 + 16 files changed, 2515 insertions(+), 97 deletions(-) create mode 100644 docs/screen-implementation-guide/03_production/production-plan-test-scenario.md create mode 100644 frontend/lib/registry/components/v2-timeline-scheduler/components/ItemTimelineCard.tsx create mode 100644 frontend/lib/registry/components/v2-timeline-scheduler/components/SchedulePreviewDialog.tsx diff --git a/backend-node/src/controllers/productionController.ts b/backend-node/src/controllers/productionController.ts index aa3f3a36..582188d6 100644 --- a/backend-node/src/controllers/productionController.ts +++ b/backend-node/src/controllers/productionController.ts @@ -95,6 +95,25 @@ export async function deletePlan(req: AuthenticatedRequest, res: Response) { } } +// ─── 자동 스케줄 미리보기 (실제 INSERT 없이 예상 결과 반환) ─── + +export async function previewSchedule(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { items, options } = req.body; + + if (!items || !Array.isArray(items) || items.length === 0) { + return res.status(400).json({ success: false, message: "품목 정보가 필요합니다" }); + } + + const data = await productionService.previewSchedule(companyCode, items, options || {}); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("자동 스케줄 미리보기 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + // ─── 자동 스케줄 생성 ─── export async function generateSchedule(req: AuthenticatedRequest, res: Response) { @@ -141,6 +160,29 @@ export async function mergeSchedules(req: AuthenticatedRequest, res: Response) { } } +// ─── 반제품 계획 미리보기 (실제 변경 없이 예상 결과) ─── + +export async function previewSemiSchedule(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const { plan_ids, options } = req.body; + + if (!plan_ids || !Array.isArray(plan_ids) || plan_ids.length === 0) { + return res.status(400).json({ success: false, message: "완제품 계획을 선택해주세요" }); + } + + const data = await productionService.previewSemiSchedule( + companyCode, + plan_ids, + options || {} + ); + return res.json({ success: true, data }); + } catch (error: any) { + logger.error("반제품 계획 미리보기 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} + // ─── 반제품 계획 자동 생성 ─── export async function generateSemiSchedule(req: AuthenticatedRequest, res: Response) { diff --git a/backend-node/src/routes/productionRoutes.ts b/backend-node/src/routes/productionRoutes.ts index 120147f0..572674aa 100644 --- a/backend-node/src/routes/productionRoutes.ts +++ b/backend-node/src/routes/productionRoutes.ts @@ -21,12 +21,18 @@ router.get("/plan/:id", productionController.getPlanById); router.put("/plan/:id", productionController.updatePlan); router.delete("/plan/:id", productionController.deletePlan); +// 자동 스케줄 미리보기 (실제 변경 없이 예상 결과) +router.post("/generate-schedule/preview", productionController.previewSchedule); + // 자동 스케줄 생성 router.post("/generate-schedule", productionController.generateSchedule); // 스케줄 병합 router.post("/merge-schedules", productionController.mergeSchedules); +// 반제품 계획 미리보기 +router.post("/generate-semi-schedule/preview", productionController.previewSemiSchedule); + // 반제품 계획 자동 생성 router.post("/generate-semi-schedule", productionController.generateSemiSchedule); diff --git a/backend-node/src/services/productionPlanService.ts b/backend-node/src/services/productionPlanService.ts index 7c8e69ec..f6b080a0 100644 --- a/backend-node/src/services/productionPlanService.ts +++ b/backend-node/src/services/productionPlanService.ts @@ -251,6 +251,101 @@ interface GenerateScheduleOptions { product_type?: string; } +/** + * 자동 스케줄 미리보기 (DB 변경 없이 예상 결과만 반환) + */ +export async function previewSchedule( + companyCode: string, + items: GenerateScheduleItem[], + options: GenerateScheduleOptions +) { + const pool = getPool(); + const productType = options.product_type || "완제품"; + const safetyLeadTime = options.safety_lead_time || 1; + + const previews: any[] = []; + const deletedSchedules: any[] = []; + const keptSchedules: any[] = []; + + for (const item of items) { + if (options.recalculate_unstarted) { + // 삭제 대상(planned) 상세 조회 + const deleteResult = await pool.query( + `SELECT id, plan_no, item_code, item_name, plan_qty, start_date, end_date, status + FROM production_plan_mng + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(product_type, '완제품') = $3 + AND status = 'planned'`, + [companyCode, item.item_code, productType] + ); + deletedSchedules.push(...deleteResult.rows); + + // 유지 대상(진행중 등) 상세 조회 + const keptResult = await pool.query( + `SELECT id, plan_no, item_code, item_name, plan_qty, start_date, end_date, status, completed_qty + FROM production_plan_mng + WHERE company_code = $1 AND item_code = $2 + AND COALESCE(product_type, '완제품') = $3 + AND status NOT IN ('planned', 'completed', 'cancelled')`, + [companyCode, item.item_code, productType] + ); + keptSchedules.push(...keptResult.rows); + } + + const dailyCapacity = item.daily_capacity || 800; + const requiredQty = item.required_qty; + if (requiredQty <= 0) continue; + + const productionDays = Math.ceil(requiredQty / dailyCapacity); + + const dueDate = new Date(item.earliest_due_date); + const endDate = new Date(dueDate); + endDate.setDate(endDate.getDate() - safetyLeadTime); + const startDate = new Date(endDate); + startDate.setDate(startDate.getDate() - productionDays); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (startDate < today) { + startDate.setTime(today.getTime()); + endDate.setTime(startDate.getTime()); + endDate.setDate(endDate.getDate() + productionDays); + } + + // 해당 품목의 수주 건수 확인 + const orderCountResult = await pool.query( + `SELECT COUNT(*) AS cnt FROM sales_order_mng + WHERE company_code = $1 AND part_code = $2 AND part_code IS NOT NULL`, + [companyCode, item.item_code] + ); + const orderCount = parseInt(orderCountResult.rows[0].cnt, 10); + + previews.push({ + item_code: item.item_code, + item_name: item.item_name, + required_qty: requiredQty, + daily_capacity: dailyCapacity, + hourly_capacity: item.hourly_capacity || 100, + production_days: productionDays, + start_date: startDate.toISOString().split("T")[0], + end_date: endDate.toISOString().split("T")[0], + due_date: item.earliest_due_date, + order_count: orderCount, + status: "planned", + }); + } + + const summary = { + total: previews.length + keptSchedules.length, + new_count: previews.length, + kept_count: keptSchedules.length, + deleted_count: deletedSchedules.length, + }; + + logger.info("자동 스케줄 미리보기", { companyCode, summary }); + return { summary, previews, deletedSchedules, keptSchedules }; +} + export async function generateSchedule( companyCode: string, items: GenerateScheduleItem[], @@ -317,14 +412,16 @@ export async function generateSchedule( endDate.setDate(endDate.getDate() + productionDays); } - // 계획번호 생성 + // 계획번호 생성 (YYYYMMDD-NNNN 형식) + const todayStr = new Date().toISOString().split("T")[0].replace(/-/g, ""); const planNoResult = await client.query( - `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no - FROM production_plan_mng WHERE company_code = $1`, - [companyCode] + `SELECT COUNT(*) + 1 AS next_no + FROM production_plan_mng + WHERE company_code = $1 AND plan_no LIKE $2`, + [companyCode, `PP-${todayStr}-%`] ); - const nextNo = planNoResult.rows[0].next_no || 1; - const planNo = `PP-${String(nextNo).padStart(6, "0")}`; + const nextNo = parseInt(planNoResult.rows[0].next_no, 10) || 1; + const planNo = `PP-${todayStr}-${String(nextNo).padStart(4, "0")}`; const insertResult = await client.query( `INSERT INTO production_plan_mng ( @@ -472,6 +569,123 @@ export async function mergeSchedules( } } +// ─── 반제품 BOM 소요량 조회 (공통) ─── + +async function getBomChildItems( + client: any, + companyCode: string, + itemCode: string +) { + const bomQuery = ` + SELECT + bd.child_item_id, + ii.item_name AS child_item_name, + ii.item_number AS child_item_code, + bd.quantity AS bom_qty, + bd.unit + FROM bom b + JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code + LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code + WHERE b.company_code = $1 + AND b.item_code = $2 + AND COALESCE(b.status, 'active') = 'active' + `; + const result = await client.query(bomQuery, [companyCode, itemCode]); + return result.rows; +} + +// ─── 반제품 계획 미리보기 (실제 DB 변경 없음) ─── + +export async function previewSemiSchedule( + companyCode: string, + planIds: number[], + options: { considerStock?: boolean; excludeUsed?: boolean } +) { + const pool = getPool(); + + const placeholders = planIds.map((_, i) => `$${i + 2}`).join(", "); + const plansResult = await pool.query( + `SELECT * FROM production_plan_mng + WHERE company_code = $1 AND id IN (${placeholders}) + AND product_type = '완제품'`, + [companyCode, ...planIds] + ); + + const previews: any[] = []; + const existingSemiPlans: any[] = []; + + for (const plan of plansResult.rows) { + // 이미 존재하는 반제품 계획 조회 + const existingResult = await pool.query( + `SELECT * FROM production_plan_mng + WHERE company_code = $1 AND parent_plan_id = $2 AND product_type = '반제품'`, + [companyCode, plan.id] + ); + existingSemiPlans.push(...existingResult.rows); + + const bomItems = await getBomChildItems(pool, companyCode, plan.item_code); + + for (const bomItem of bomItems) { + let requiredQty = (parseFloat(plan.plan_qty) || 0) * (parseFloat(bomItem.bom_qty) || 1); + + if (options.considerStock) { + const stockResult = await pool.query( + `SELECT COALESCE(SUM(CAST(current_qty AS numeric)), 0) AS stock + FROM inventory_stock + WHERE company_code = $1 AND item_code = $2`, + [companyCode, bomItem.child_item_code || bomItem.child_item_id] + ); + const stock = parseFloat(stockResult.rows[0].stock) || 0; + requiredQty = Math.max(requiredQty - stock, 0); + } + + if (requiredQty <= 0) continue; + + const semiDueDate = plan.start_date; + const semiStartDate = new Date(plan.start_date); + semiStartDate.setDate(semiStartDate.getDate() - (parseInt(plan.lead_time) || 1)); + + previews.push({ + parent_plan_id: plan.id, + parent_plan_no: plan.plan_no, + parent_item_name: plan.item_name, + item_code: bomItem.child_item_code || bomItem.child_item_id, + item_name: bomItem.child_item_name || bomItem.child_item_id, + plan_qty: requiredQty, + bom_qty: parseFloat(bomItem.bom_qty) || 1, + start_date: semiStartDate.toISOString().split("T")[0], + end_date: typeof semiDueDate === "string" + ? semiDueDate.split("T")[0] + : new Date(semiDueDate).toISOString().split("T")[0], + due_date: typeof semiDueDate === "string" + ? semiDueDate.split("T")[0] + : new Date(semiDueDate).toISOString().split("T")[0], + product_type: "반제품", + status: "planned", + }); + } + } + + // 기존 반제품 중 삭제 대상 (status = planned) + const deletedSchedules = existingSemiPlans.filter( + (s) => s.status === "planned" + ); + // 기존 반제품 중 유지 대상 (진행중 등) + const keptSchedules = existingSemiPlans.filter( + (s) => s.status !== "planned" && s.status !== "completed" + ); + + const summary = { + total: previews.length + keptSchedules.length, + new_count: previews.length, + deleted_count: deletedSchedules.length, + kept_count: keptSchedules.length, + parent_count: plansResult.rowCount, + }; + + return { summary, previews, deletedSchedules, keptSchedules }; +} + // ─── 반제품 계획 자동 생성 ─── export async function generateSemiSchedule( @@ -486,41 +700,36 @@ export async function generateSemiSchedule( try { await client.query("BEGIN"); - // 선택된 완제품 계획 조회 const placeholders = planIds.map((_, i) => `$${i + 2}`).join(", "); const plansResult = await client.query( `SELECT * FROM production_plan_mng - WHERE company_code = $1 AND id IN (${placeholders})`, + WHERE company_code = $1 AND id IN (${placeholders}) + AND product_type = '완제품'`, [companyCode, ...planIds] ); + // 기존 planned 상태 반제품 삭제 + for (const plan of plansResult.rows) { + await client.query( + `DELETE FROM production_plan_mng + WHERE company_code = $1 AND parent_plan_id = $2 + AND product_type = '반제품' AND status = 'planned'`, + [companyCode, plan.id] + ); + } + const newSemiPlans: any[] = []; + const todayStr = new Date().toISOString().split("T")[0].replace(/-/g, ""); for (const plan of plansResult.rows) { - // BOM에서 해당 품목의 반제품 소요량 조회 - const bomQuery = ` - SELECT - bd.child_item_id, - ii.item_name AS child_item_name, - ii.item_code AS child_item_code, - bd.quantity AS bom_qty, - bd.unit - FROM bom b - JOIN bom_detail bd ON b.id = bd.bom_id AND b.company_code = bd.company_code - LEFT JOIN item_info ii ON bd.child_item_id = ii.id AND bd.company_code = ii.company_code - WHERE b.company_code = $1 - AND b.item_code = $2 - AND COALESCE(b.status, 'active') = 'active' - `; - const bomResult = await client.query(bomQuery, [companyCode, plan.item_code]); + const bomItems = await getBomChildItems(client, companyCode, plan.item_code); - for (const bomItem of bomResult.rows) { + for (const bomItem of bomItems) { let requiredQty = (parseFloat(plan.plan_qty) || 0) * (parseFloat(bomItem.bom_qty) || 1); - // 재고 고려 if (options.considerStock) { const stockResult = await client.query( - `SELECT COALESCE(SUM(current_qty::numeric), 0) AS stock + `SELECT COALESCE(SUM(CAST(current_qty AS numeric)), 0) AS stock FROM inventory_stock WHERE company_code = $1 AND item_code = $2`, [companyCode, bomItem.child_item_code || bomItem.child_item_id] @@ -531,18 +740,20 @@ export async function generateSemiSchedule( if (requiredQty <= 0) continue; - // 반제품 납기일 = 완제품 시작일 const semiDueDate = plan.start_date; const semiEndDate = plan.start_date; const semiStartDate = new Date(plan.start_date); - semiStartDate.setDate(semiStartDate.getDate() - (plan.lead_time || 1)); + semiStartDate.setDate(semiStartDate.getDate() - (parseInt(plan.lead_time) || 1)); + // plan_no 생성 (PP-YYYYMMDD-SXXX 형식, S = 반제품) const planNoResult = await client.query( - `SELECT COALESCE(MAX(CAST(REPLACE(plan_no, 'PP-', '') AS INTEGER)), 0) + 1 AS next_no - FROM production_plan_mng WHERE company_code = $1`, - [companyCode] + `SELECT COUNT(*) + 1 AS next_no + FROM production_plan_mng + WHERE company_code = $1 AND plan_no LIKE $2`, + [companyCode, `PP-${todayStr}-S%`] ); - const planNo = `PP-${String(planNoResult.rows[0].next_no || 1).padStart(6, "0")}`; + const nextNo = parseInt(planNoResult.rows[0].next_no, 10) || 1; + const planNo = `PP-${todayStr}-S${String(nextNo).padStart(3, "0")}`; const insertResult = await client.query( `INSERT INTO production_plan_mng ( @@ -560,8 +771,8 @@ export async function generateSemiSchedule( bomItem.child_item_name || bomItem.child_item_id, requiredQty, semiStartDate.toISOString().split("T")[0], - typeof semiEndDate === "string" ? semiEndDate : semiEndDate.toISOString().split("T")[0], - typeof semiDueDate === "string" ? semiDueDate : semiDueDate.toISOString().split("T")[0], + typeof semiEndDate === "string" ? semiEndDate.split("T")[0] : new Date(semiEndDate).toISOString().split("T")[0], + typeof semiDueDate === "string" ? semiDueDate.split("T")[0] : new Date(semiDueDate).toISOString().split("T")[0], plan.id, createdBy, ] diff --git a/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md index 84f6c789..55740e97 100644 --- a/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md +++ b/docs/screen-implementation-guide/00_analysis/next-component-development-plan.md @@ -548,11 +548,11 @@ function detectConflicts(schedules: ScheduleItem[], resourceId: string): Schedul - [x] 범례 표시 (TimelineLegend: 상태별 색상 + 마일스톤 + 충돌) - [x] 반응형 공통 CSS 적용 (text-[10px] sm:text-sm 패턴) - [x] staticFilters 지원 (커스텀 테이블 필터링) -- [ ] 가상 스크롤 (향후 - 대용량 100+ 리소스) +- [x] 가상 스크롤 (@tanstack/react-virtual, 30개 이상 리소스 시 자동 활성화) - [x] 설정 패널 구현 - [x] API 연동 - [x] 레지스트리 등록 -- [ ] 테스트 완료 +- [x] 테스트 완료 (20개 테스트 전체 통과 - 충돌감지 11건 + 날짜계산 9건) - [x] 문서화 (README.md) --- diff --git a/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md index 02699843..69f9b3d5 100644 --- a/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md +++ b/docs/screen-implementation-guide/00_analysis/schedule-auto-generation-guide.md @@ -892,3 +892,79 @@ if (process.env.NODE_ENV === "development") { }); } ``` + +--- + +## 12. 생산기간(리드타임) 산출 - 현재 상태 및 개선 방안 + +> 작성일: 2026-03-16 | 상태: 검토 대기 (스키마 변경 전 상의 필요) + +### 12.1 현재 구현 상태 + +**생산일수 계산 로직** (`productionPlanService.ts`): + +``` +생산일수 = ceil(계획수량 / 일생산능력) +종료일 = 납기일 - 안전리드타임 +시작일 = 종료일 - 생산일수 +``` + +**현재 기본값 (하드코딩):** + +| 항목 | 현재값 | 위치 | +|------|--------|------| +| 일생산능력 (daily_capacity) | 800 EA/일 | `productionPlanService.ts` 기본값 | +| 시간당 능력 (hourly_capacity) | 100 EA/시간 | `productionPlanService.ts` 기본값 | +| 안전리드타임 (safety_lead_time) | 1일 | 옵션 기본값 | +| 반제품 리드타임 (lead_time) | 1일 | `production_plan_mng` 기본값 | + +**문제점:** +- `item_info`에 생산 파라미터 컬럼이 없음 +- 모든 품목이 동일한 기본값(800EA/일)으로 계산됨 +- 업체별/품목별 생산능력 차이를 반영 불가 + +### 12.2 개선 방향 (상의 후 결정) + +**1단계 (품목 마스터 기반) - 권장:** + +`item_info` 테이블에 컬럼 추가: +- `lead_time_days`: 리드타임 (일) +- `daily_capacity`: 일생산능력 +- `min_lot_size`: 최소 생산 단위 (선택) +- `setup_time`: 셋업시간 (선택) + +자동 스케줄 생성 시 품목 마스터 조회 → 값 없으면 기본값 사용 (하위 호환) + +**2단계 (설비별 능력) - 고객 요청 시:** + +별도 테이블 `item_equipment_capacity`: +- 품목 + 설비 조합별 생산능력 관리 +- 동일 품목이라도 설비에 따라 능력 다를 때 + +**3단계 (공정 라우팅) - 대기업 대응:** + +공정 순서 + 공정별 소요시간 전체 관리 +- 현재 시점에서는 불필요 + +### 12.3 반제품 계획 생성 현황 + +**구현 완료 항목:** +- API: `POST /production/generate-semi-schedule/preview` (미리보기) +- API: `POST /production/generate-semi-schedule` (실제 생성) +- BOM 기반 소요량 자동 계산 +- 타임라인 컴포넌트 내 "반제품 계획 생성" 버튼 (완제품 탭에서만 표시) +- 반제품 탭: linkedFilter 제거, staticFilters만 사용 (전체 반제품 표시) + +**반제품 생산기간 계산:** +- 반제품 납기일 = 완제품 시작일 +- 반제품 시작일 = 완제품 시작일 - lead_time (기본 1일) +- BOM 소요량 = 완제품 계획수량 x BOM 수량 + +**테스트 BOM 데이터:** + +| 완제품 | 반제품 | BOM 수량 | +|--------|--------|----------| +| ITEM-001 (탑씰 Type A) | SEMI-001 (탑씰 필름 A) | 2 EA/개 | +| ITEM-001 (탑씰 Type A) | SEMI-002 (탑씰 접착제) | 0.5 KG/개 | +| ITEM-002 (탑씰 Type B) | SEMI-003 (탑씰 필름 B) | 3 EA/개 | +| ITEM-002 (탑씰 Type B) | SEMI-004 (탑씰 코팅제) | 0.3 KG/개 | diff --git a/docs/screen-implementation-guide/01_reference_guides/full-screen-analysis.md b/docs/screen-implementation-guide/01_reference_guides/full-screen-analysis.md index 1ba0da01..a648e309 100644 --- a/docs/screen-implementation-guide/01_reference_guides/full-screen-analysis.md +++ b/docs/screen-implementation-guide/01_reference_guides/full-screen-analysis.md @@ -1,6 +1,6 @@ # WACE 화면 시스템 - DB 스키마 & 컴포넌트 설정 전체 레퍼런스 -> **최종 업데이트**: 2026-03-13 +> **최종 업데이트**: 2026-03-16 > **용도**: AI 챗봇이 화면 생성 시 참조하는 DB 스키마, 컴포넌트 전체 설정 사전 > **관련 문서**: `v2-component-usage-guide.md` (SQL 템플릿, 실행 예시) @@ -532,15 +532,20 @@ CREATE TABLE "{테이블명}" ( --- -### 3.11 v2-timeline-scheduler (간트차트) +### 3.11 v2-timeline-scheduler (간트차트/타임라인) -**용도**: 시간축 기반 일정/계획 시각화. 드래그/리사이즈로 일정 편집. +**용도**: 시간축 기반 일정/계획 시각화. 드래그/리사이즈로 일정 편집. 품목별 그룹 뷰, 자동 스케줄 생성, 반제품 계획 연동 지원. + +**기본 설정**: | 설정 | 타입 | 기본값 | 설명 | |------|------|--------|------| | selectedTable | string | - | 스케줄 데이터 테이블 | +| customTableName | string | - | selectedTable 대신 사용 (useCustomTable=true 시) | +| useCustomTable | boolean | `false` | customTableName 사용 여부 | | resourceTable | string | `"equipment_mng"` | 리소스(설비/작업자) 테이블 | | scheduleType | string | `"PRODUCTION"` | 스케줄 유형: `PRODUCTION`/`MAINTENANCE`/`SHIPPING`/`WORK_ASSIGN` | +| viewMode | string | - | 뷰 모드: `"itemGrouped"` (품목별 카드 그룹) / 미설정 시 리소스 기반 | | defaultZoomLevel | string | `"day"` | 초기 줌: `day`/`week`/`month` | | editable | boolean | `true` | 편집 가능 | | draggable | boolean | `true` | 드래그 이동 허용 | @@ -548,15 +553,16 @@ CREATE TABLE "{테이블명}" ( | rowHeight | number | `50` | 행 높이(px) | | headerHeight | number | `60` | 헤더 높이(px) | | resourceColumnWidth | number | `150` | 리소스 컬럼 너비(px) | -| cellWidth.day | number | `60` | 일 단위 셀 너비 | -| cellWidth.week | number | `120` | 주 단위 셀 너비 | -| cellWidth.month | number | `40` | 월 단위 셀 너비 | | showConflicts | boolean | `true` | 시간 겹침 충돌 표시 | | showProgress | boolean | `true` | 진행률 바 표시 | | showTodayLine | boolean | `true` | 오늘 날짜 표시선 | | showToolbar | boolean | `true` | 상단 툴바 표시 | +| showLegend | boolean | `true` | 범례(상태 색상 안내) 표시 | +| showNavigation | boolean | `true` | 날짜 네비게이션 버튼 표시 | +| showZoomControls | boolean | `true` | 줌 컨트롤 버튼 표시 | | showAddButton | boolean | `true` | 추가 버튼 | | height | number | `500` | 높이(px) | +| maxHeight | number | - | 최대 높이(px) | **fieldMapping (필수)**: @@ -583,10 +589,74 @@ CREATE TABLE "{테이블명}" ( | 상태 | 기본 색상 | |------|----------| | planned | `"#3b82f6"` (파랑) | -| in_progress | `"#f59e0b"` (주황) | -| completed | `"#10b981"` (초록) | +| in_progress | `"#10b981"` (초록) | +| completed | `"#6b7280"` (회색) | | delayed | `"#ef4444"` (빨강) | -| cancelled | `"#6b7280"` (회색) | +| cancelled | `"#9ca3af"` (연회색) | + +**staticFilters (정적 필터)** - DB 조회 시 항상 적용되는 WHERE 조건: + +| 설정 | 타입 | 설명 | +|------|------|------| +| product_type | string | `"완제품"` 또는 `"반제품"` 등 고정 필터 | +| status | string | 상태값 필터 | +| (임의 컬럼) | string | 해당 컬럼으로 필터링 | + +```json +"staticFilters": { + "product_type": "완제품" +} +``` + +**linkedFilter (연결 필터)** - 다른 컴포넌트(주로 테이블)의 선택 이벤트와 연동: + +| 설정 | 타입 | 설명 | +|------|------|------| +| sourceField | string | 소스 컴포넌트(좌측 테이블)의 필터 기준 컬럼 | +| targetField | string | 타임라인 스케줄 데이터에서 매칭할 컬럼 | +| sourceTableName | string | 이벤트 발신 테이블명 (이벤트 필터용) | +| sourceComponentId | string | 이벤트 발신 컴포넌트 ID (선택) | +| emptyMessage | string | 선택 전 빈 상태 메시지 | +| showEmptyWhenNoSelection | boolean | 선택 전 빈 상태 표시 여부 | + +```json +"linkedFilter": { + "sourceField": "part_code", + "targetField": "item_code", + "sourceTableName": "sales_order_mng", + "emptyMessage": "좌측 수주 목록에서 품목을 선택하세요", + "showEmptyWhenNoSelection": true +} +``` + +> **linkedFilter 동작 원리**: v2EventBus의 `TABLE_SELECTION_CHANGE` 이벤트를 구독. +> 좌측 테이블에서 행을 선택하면 해당 행의 `sourceField` 값을 수집하여, +> 타임라인 데이터 중 `targetField`가 일치하는 스케줄만 클라이언트 측에서 필터링 표시. +> `staticFilters`는 서버 측 조회, `linkedFilter`는 클라이언트 측 필터링. + +**viewMode: "itemGrouped" (품목별 그룹 뷰)**: + +리소스(설비) 기반 간트차트 대신, 품목(item_code)별로 카드를 그룹화하여 표시하는 모드. +각 카드 안에 해당 품목의 스케줄 바가 미니 타임라인으로 표시됨. + +설정 시 `viewMode: "itemGrouped"`만 추가하면 됨. 툴바에 자동으로: +- 날짜 네비게이션 (이전/오늘/다음) +- 줌 컨트롤 +- 새로고침 버튼 +- (완제품 탭일 때) **완제품 계획 생성** / **반제품 계획 생성** 버튼 + +**자동 스케줄 생성 (내장 기능)**: + +`viewMode: "itemGrouped"` + `staticFilters.product_type === "완제품"` 일 때 자동 활성화. + +- **완제품 계획 생성**: linkedFilter로 선택된 수주 품목 기반, 미리보기 다이얼로그 → 확인 후 생성 + - API: `POST /production/generate-schedule/preview` → `POST /production/generate-schedule` +- **반제품 계획 생성**: 현재 타임라인의 완제품 스케줄 기반, BOM 소요량으로 반제품 계획 미리보기 → 확인 후 생성 + - API: `POST /production/generate-semi-schedule/preview` → `POST /production/generate-semi-schedule` + +> **중요**: 반제품 전용 타임라인에는 `linkedFilter`를 걸지 않는다. +> 반제품 item_code가 수주 품목 코드와 다르므로 매칭 불가. +> `staticFilters: { product_type: "반제품" }`만 설정하여 전체 반제품 계획을 표시. --- @@ -923,16 +993,32 @@ CREATE TABLE "{테이블명}" ( ## 4. 패턴 의사결정 트리 ``` -Q1. 시간축 기반 일정/간트차트? → v2-timeline-scheduler -Q2. 다차원 피벗 분석? → v2-pivot-grid -Q3. 그룹별 접기/펼치기? → v2-table-grouped -Q4. 카드 형태 표시? → v2-card-display -Q5. 마스터-디테일? +Q1. 좌측 마스터 + 우측 탭(타임라인/테이블) 복합 구성? + → 패턴 F → v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler +Q2. 시간축 기반 일정/간트차트? + ├ 품목별 카드 그룹 뷰? → 패턴 E-2 → v2-timeline-scheduler(viewMode:itemGrouped) + └ 리소스(설비) 기반? → 패턴 E → v2-timeline-scheduler +Q3. 다차원 피벗 분석? → v2-pivot-grid +Q4. 그룹별 접기/펼치기? → v2-table-grouped +Q5. 카드 형태 표시? → v2-card-display +Q6. 마스터-디테일? ├ 우측 멀티 탭? → v2-split-panel-layout + additionalTabs └ 단일 디테일? → v2-split-panel-layout -Q6. 단일 테이블? → v2-table-search-widget + v2-table-list +Q7. 단일 테이블? → v2-table-search-widget + v2-table-list ``` +### 패턴 요약표 + +| 패턴 | 대표 화면 | 핵심 컴포넌트 | +|------|----------|-------------| +| A | 거래처관리 | v2-table-search-widget + v2-table-list | +| B | 수주관리 | v2-split-panel-layout | +| C | 수주관리(멀티탭) | v2-split-panel-layout + additionalTabs | +| D | 재고현황 | v2-table-grouped | +| E | 설비 작업일정 | v2-timeline-scheduler (리소스 기반) | +| E-2 | 품목별 타임라인 | v2-timeline-scheduler (viewMode: itemGrouped) | +| F | 생산계획 | split(custom) + tabs + timeline | + --- ## 5. 관계(relation) 레퍼런스 diff --git a/docs/screen-implementation-guide/01_reference_guides/v2-component-usage-guide.md b/docs/screen-implementation-guide/01_reference_guides/v2-component-usage-guide.md index 14182a91..6d9f7c8a 100644 --- a/docs/screen-implementation-guide/01_reference_guides/v2-component-usage-guide.md +++ b/docs/screen-implementation-guide/01_reference_guides/v2-component-usage-guide.md @@ -1,6 +1,6 @@ # WACE 화면 구현 실행 가이드 (챗봇/AI 에이전트 전용) -> **최종 업데이트**: 2026-03-13 +> **최종 업데이트**: 2026-03-16 > **용도**: 사용자가 "수주관리 화면 만들어줘"라고 요청하면, 이 문서를 참조하여 SQL을 직접 생성하고 화면을 구현하는 AI 챗봇용 실행 가이드 > **핵심**: 이 문서의 SQL 템플릿을 따라 INSERT하면 화면이 자동으로 생성된다 @@ -533,7 +533,9 @@ DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now(); } ``` -### 8.5 패턴 E: 타임라인/간트차트 +### 8.5 패턴 E: 타임라인/간트차트 (리소스 기반) + +**사용 조건**: 설비/작업자 등 리소스 기준으로 스케줄을 시간축에 표시 ```json { @@ -575,6 +577,246 @@ DO UPDATE SET layout_data = EXCLUDED.layout_data, updated_at = now(); } ``` +### 8.6 패턴 E-2: 타임라인 (품목 그룹 뷰 + 연결 필터) + +**사용 조건**: 좌측 테이블에서 선택한 품목 기반으로 타임라인을 필터링 표시. 품목별 카드 그룹 뷰. + +> 리소스(설비) 기반이 아닌, **품목(item_code)별로 카드 그룹** 형태로 스케줄을 표시한다. +> 좌측 테이블에서 행을 선택하면 `linkedFilter`로 해당 품목의 스케줄만 필터링. +> `staticFilters`로 완제품/반제품 등 데이터 유형을 고정 필터링. + +```json +{ + "id": "timeline_finished", + "url": "@/lib/registry/components/v2-timeline-scheduler", + "position": { "x": 0, "y": 0 }, + "size": { "width": 1920, "height": 800 }, + "displayOrder": 0, + "overrides": { + "label": "완제품 생산계획", + "selectedTable": "{스케줄_테이블}", + "viewMode": "itemGrouped", + "fieldMapping": { + "id": "id", + "resourceId": "item_code", + "title": "item_name", + "startDate": "start_date", + "endDate": "end_date", + "status": "status" + }, + "defaultZoomLevel": "day", + "staticFilters": { + "product_type": "완제품" + }, + "linkedFilter": { + "sourceField": "part_code", + "targetField": "item_code", + "sourceTableName": "{좌측_테이블명}", + "emptyMessage": "좌측 목록에서 품목을 선택하세요", + "showEmptyWhenNoSelection": true + } + } +} +``` + +**핵심 설정 설명**: + +| 설정 | 용도 | +|------|------| +| `viewMode: "itemGrouped"` | 리소스 행이 아닌, 품목별 카드 그룹으로 표시 | +| `staticFilters` | DB 조회 시 항상 적용 (서버측 WHERE 조건) | +| `linkedFilter` | 다른 컴포넌트 선택 이벤트로 클라이언트 측 필터링 | +| `linkedFilter.sourceField` | 소스 테이블에서 가져올 값의 컬럼명 | +| `linkedFilter.targetField` | 타임라인 데이터에서 매칭할 컬럼명 | + +> **주의**: `linkedFilter`와 `staticFilters`의 차이 +> - `staticFilters`: DB SELECT 쿼리의 WHERE 절에 포함 → 서버에서 필터링 +> - `linkedFilter`: 전체 데이터를 불러온 후, 선택 이벤트에 따라 클라이언트에서 필터링 + +### 8.7 패턴 F: 복합 화면 (좌측 테이블 + 우측 탭 내 타임라인) + +**사용 조건**: 생산계획처럼 좌측 마스터 테이블 + 우측에 탭으로 여러 타임라인/테이블을 표시하는 복합 화면. +`v2-split-panel-layout`의 `rightPanel.displayMode: "custom"` + `v2-tabs-widget` + `v2-timeline-scheduler` 조합. + +**구조 개요**: + +``` +┌──────────────────────────────────────────────────┐ +│ v2-split-panel-layout │ +│ ┌──────────┬─────────────────────────────────┐ │ +│ │ leftPanel │ rightPanel (displayMode:custom)│ │ +│ │ │ ┌─────────────────────────────┐│ │ +│ │ v2-table- │ │ v2-tabs-widget ││ │ +│ │ grouped │ │ ┌───────┬───────┬─────────┐ ││ │ +│ │ (수주목록) │ │ │완제품 │반제품 │기타 탭 │ ││ │ +│ │ │ │ └───────┴───────┴─────────┘ ││ │ +│ │ │ │ ┌─────────────────────────┐ ││ │ +│ │ │ │ │ v2-timeline-scheduler │ ││ │ +│ │ │ │ │ (품목별 그룹 뷰) │ ││ │ +│ │ │ │ └─────────────────────────┘ ││ │ +│ │ │ └─────────────────────────────┘│ │ +│ └──────────┴─────────────────────────────────┘ │ +└──────────────────────────────────────────────────┘ +``` + +**실제 layout_data 예시** (생산계획 화면 참고): + +```json +{ + "version": "2.0", + "components": [ + { + "id": "split_pp", + "url": "@/lib/registry/components/v2-split-panel-layout", + "position": { "x": 0, "y": 0 }, + "size": { "width": 1920, "height": 850 }, + "displayOrder": 0, + "overrides": { + "label": "생산계획", + "splitRatio": 25, + "resizable": true, + "autoLoad": true, + "syncSelection": true, + "leftPanel": { + "title": "수주 목록", + "displayMode": "custom", + "components": [ + { + "id": "grouped_orders", + "componentType": "v2-table-grouped", + "label": "수주별 품목", + "position": { "x": 0, "y": 0 }, + "size": { "width": 600, "height": 800 }, + "componentConfig": { + "selectedTable": "sales_order_mng", + "groupConfig": { + "groupByColumn": "order_number", + "groupLabelFormat": "{value}", + "defaultExpanded": true, + "summary": { "showCount": true } + }, + "columns": [ + { "columnName": "part_code", "displayName": "품번", "visible": true, "width": 100 }, + { "columnName": "part_name", "displayName": "품명", "visible": true, "width": 120 }, + { "columnName": "order_qty", "displayName": "수량", "visible": true, "width": 60 }, + { "columnName": "delivery_date", "displayName": "납기일", "visible": true, "width": 90 } + ], + "showCheckbox": true, + "checkboxMode": "multi" + } + } + ] + }, + "rightPanel": { + "title": "생산 계획", + "displayMode": "custom", + "components": [ + { + "id": "tabs_pp", + "componentType": "v2-tabs-widget", + "label": "생산계획 탭", + "position": { "x": 0, "y": 0 }, + "size": { "width": 1400, "height": 800 }, + "componentConfig": { + "tabs": [ + { + "id": "tab_finished", + "label": "완제품", + "order": 1, + "components": [ + { + "id": "timeline_finished", + "componentType": "v2-timeline-scheduler", + "label": "완제품 타임라인", + "position": { "x": 0, "y": 0 }, + "size": { "width": 1380, "height": 750 }, + "componentConfig": { + "selectedTable": "production_plan_mng", + "viewMode": "itemGrouped", + "fieldMapping": { + "id": "id", + "resourceId": "item_code", + "title": "item_name", + "startDate": "start_date", + "endDate": "end_date", + "status": "status" + }, + "defaultZoomLevel": "day", + "staticFilters": { + "product_type": "완제품" + }, + "linkedFilter": { + "sourceField": "part_code", + "targetField": "item_code", + "sourceTableName": "sales_order_mng", + "emptyMessage": "좌측 수주 목록에서 품목을 선택하세요", + "showEmptyWhenNoSelection": true + } + } + } + ] + }, + { + "id": "tab_semi", + "label": "반제품", + "order": 2, + "components": [ + { + "id": "timeline_semi", + "componentType": "v2-timeline-scheduler", + "label": "반제품 타임라인", + "position": { "x": 0, "y": 0 }, + "size": { "width": 1380, "height": 750 }, + "componentConfig": { + "selectedTable": "production_plan_mng", + "viewMode": "itemGrouped", + "fieldMapping": { + "id": "id", + "resourceId": "item_code", + "title": "item_name", + "startDate": "start_date", + "endDate": "end_date", + "status": "status" + }, + "defaultZoomLevel": "day", + "staticFilters": { + "product_type": "반제품" + } + } + } + ] + } + ], + "defaultTab": "tab_finished" + } + } + ] + } + } + } + ], + "gridSettings": { "columns": 12, "gap": 16, "padding": 16 }, + "screenResolution": { "width": 1920, "height": 1080 } +} +``` + +**패턴 F 핵심 포인트**: + +| 포인트 | 설명 | +|--------|------| +| `leftPanel.displayMode: "custom"` | 좌측에 v2-table-grouped 등 자유 배치 | +| `rightPanel.displayMode: "custom"` | 우측에 v2-tabs-widget 등 자유 배치 | +| `componentConfig` | custom 내부 컴포넌트는 overrides 대신 componentConfig 사용 | +| `componentType` | custom 내부에서는 url 대신 componentType 사용 | +| 완제품 탭에만 `linkedFilter` | 좌측 테이블과 연동 필터링 | +| 반제품 탭에는 `linkedFilter` 없음 | 반제품 item_code가 수주 품목과 다르므로 전체 표시 | +| 자동 스케줄 생성 버튼 | `staticFilters.product_type === "완제품"` 일 때 자동 표시 | + +> **displayMode: "custom" 내부 컴포넌트 규칙**: +> - `url` 대신 `componentType` 사용 (예: `"v2-timeline-scheduler"`, `"v2-table-grouped"`) +> - `overrides` 대신 `componentConfig` 사용 +> - `position`, `size`는 동일하게 사용 + --- ## 9. Step 7: menu_info INSERT @@ -696,29 +938,47 @@ VALUES 사용자가 화면을 요청하면 이 트리로 패턴을 결정한다. ``` -Q1. 시간축 기반 일정/간트차트가 필요한가? -├─ YES → 패턴 E (타임라인) → v2-timeline-scheduler +Q1. 좌측 마스터 + 우측에 탭으로 타임라인/테이블 등 복합 구성이 필요한가? +├─ YES → 패턴 F (복합 화면) → v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler └─ NO ↓ -Q2. 다차원 집계/피벗 분석이 필요한가? +Q2. 시간축 기반 일정/간트차트가 필요한가? +├─ YES → Q2-1. 품목별 카드 그룹 뷰인가? +│ ├─ YES → 패턴 E-2 (품목 그룹 타임라인) → v2-timeline-scheduler(viewMode:itemGrouped) +│ └─ NO → 패턴 E (리소스 기반 타임라인) → v2-timeline-scheduler +└─ NO ↓ + +Q3. 다차원 집계/피벗 분석이 필요한가? ├─ YES → 피벗 → v2-pivot-grid └─ NO ↓ -Q3. 데이터를 그룹별로 접기/펼치기가 필요한가? +Q4. 데이터를 그룹별로 접기/펼치기가 필요한가? ├─ YES → 패턴 D (그룹화) → v2-table-grouped └─ NO ↓ -Q4. 이미지+정보를 카드 형태로 표시하는가? +Q5. 이미지+정보를 카드 형태로 표시하는가? ├─ YES → 카드뷰 → v2-card-display └─ NO ↓ -Q5. 마스터 테이블 선택 시 연관 디테일이 필요한가? -├─ YES → Q5-1. 디테일에 여러 탭이 필요한가? +Q6. 마스터 테이블 선택 시 연관 디테일이 필요한가? +├─ YES → Q6-1. 디테일에 여러 탭이 필요한가? │ ├─ YES → 패턴 C (마스터-디테일+탭) → v2-split-panel-layout + additionalTabs │ └─ NO → 패턴 B (마스터-디테일) → v2-split-panel-layout └─ NO → 패턴 A (기본 마스터) → v2-table-search-widget + v2-table-list ``` +### 패턴 선택 빠른 참조 + +| 패턴 | 대표 화면 | 핵심 컴포넌트 | +|------|----------|-------------| +| A | 거래처관리, 코드관리 | v2-table-search-widget + v2-table-list | +| B | 수주관리, 발주관리 | v2-split-panel-layout | +| C | 수주관리(멀티탭) | v2-split-panel-layout + additionalTabs | +| D | 재고현황, 그룹별조회 | v2-table-grouped | +| E | 설비 작업일정 | v2-timeline-scheduler (리소스 기반) | +| E-2 | 단독 품목별 타임라인 | v2-timeline-scheduler (viewMode: itemGrouped) | +| F | 생산계획, 작업지시 | v2-split-panel-layout(custom) + v2-tabs-widget + v2-timeline-scheduler | + --- ## 13. 화면 간 연결 관계 정의 @@ -1119,7 +1379,8 @@ VALUES ( | 검색 바 | v2-table-search-widget | `autoSelectFirstTable` | | 좌우 분할 | v2-split-panel-layout | `leftPanel`, `rightPanel`, `relation`, `splitRatio` | | 그룹화 테이블 | v2-table-grouped | `groupConfig.groupByColumn`, `summary` | -| 간트차트 | v2-timeline-scheduler | `fieldMapping`, `resourceTable` | +| 간트차트 (리소스 기반) | v2-timeline-scheduler | `fieldMapping`, `resourceTable` | +| 타임라인 (품목 그룹) | v2-timeline-scheduler | `viewMode:"itemGrouped"`, `staticFilters`, `linkedFilter` | | 피벗 분석 | v2-pivot-grid | `fields(area, summaryType)` | | 카드 뷰 | v2-card-display | `columnMapping`, `cardsPerRow` | | 액션 버튼 | v2-button-primary | `text`, `actionType`, `webTypeConfig.dataflowConfig` | @@ -1144,3 +1405,97 @@ VALUES ( | 창고 랙 | v2-rack-structure | `codePattern`, `namePattern`, `maxRows` | | 공정 작업기준 | v2-process-work-standard | `dataSource.itemTable`, `dataSource.routingDetailTable` | | 품목 라우팅 | v2-item-routing | `dataSource.itemTable`, `dataSource.routingDetailTable` | + +--- + +## 17. v2-timeline-scheduler 고급 설정 가이드 + +### 17.1 viewMode 선택 기준 + +| viewMode | 용도 | Y축 | +|----------|------|-----| +| (미설정) | 설비별 작업일정, 보전계획 | 설비/작업자 행 | +| `"itemGrouped"` | 생산계획, 출하계획 | 품목별 카드 그룹 | + +### 17.2 staticFilters vs linkedFilter 비교 + +| 구분 | staticFilters | linkedFilter | +|------|--------------|-------------| +| **적용 시점** | DB SELECT 쿼리 시 | 클라이언트 렌더링 시 | +| **위치** | 서버 측 (WHERE 절) | 프론트 측 (JS 필터링) | +| **변경 가능** | 고정 (layout에 하드코딩) | 동적 (이벤트 기반) | +| **용도** | 완제품/반제품 구분 등 | 좌측 테이블 선택 연동 | + +**조합 예시**: +``` +staticFilters: { product_type: "완제품" } → DB에서 완제품만 조회 +linkedFilter: { sourceField: "part_code", targetField: "item_code" } + → 완제품 중 좌측에서 선택한 품목만 표시 +``` + +### 17.3 자동 스케줄 생성 (내장 기능) + +`viewMode: "itemGrouped"` + `staticFilters.product_type === "완제품"` 조건 충족 시, +타임라인 툴바에 **완제품 계획 생성** / **반제품 계획 생성** 버튼이 자동 표시됨. + +**완제품 계획 생성 플로우**: +``` +1. linkedFilter로 선택된 수주 품목 수집 +2. POST /production/generate-schedule/preview → 미리보기 다이얼로그 +3. 사용자 확인 → POST /production/generate-schedule → 실제 생성 +4. 타임라인 자동 새로고침 +``` + +**반제품 계획 생성 플로우**: +``` +1. 현재 타임라인의 완제품 스케줄 ID 수집 +2. POST /production/generate-semi-schedule/preview → BOM 기반 소요량 계산 +3. 미리보기 다이얼로그 (기존 반제품 계획 삭제/유지 정보 포함) +4. 사용자 확인 → POST /production/generate-semi-schedule → 실제 생성 +5. 반제품 탭으로 전환 시 새 데이터 표시 +``` + +### 17.4 반제품 탭 주의사항 + +반제품 전용 타임라인에는 `linkedFilter`를 **걸지 않는다**. + +이유: 반제품의 `item_code`(예: `SEMI-001`)와 수주 품목의 `part_code`(예: `ITEM-001`)가 +서로 다른 값이므로 매칭이 불가능하다. `staticFilters: { product_type: "반제품" }`만 설정. + +```json +{ + "id": "timeline_semi", + "componentType": "v2-timeline-scheduler", + "componentConfig": { + "selectedTable": "production_plan_mng", + "viewMode": "itemGrouped", + "staticFilters": { "product_type": "반제품" }, + "fieldMapping": { "..." : "..." } + } +} +``` + +### 17.5 이벤트 연동 (v2EventBus) + +타임라인 컴포넌트는 `v2EventBus`를 통해 다른 컴포넌트와 통신한다. + +| 이벤트 | 방향 | 설명 | +|--------|------|------| +| `TABLE_SELECTION_CHANGE` | 수신 | 좌측 테이블 행 선택 시 linkedFilter 적용 | +| `TIMELINE_REFRESH` | 발신/수신 | 타임라인 데이터 새로고침 | + +**연결 필터 이벤트 페이로드**: +```typescript +{ + eventType: "TABLE_SELECTION_CHANGE", + source: "grouped_orders", + tableName: "sales_order_mng", + selectedRows: [ + { id: "...", part_code: "ITEM-001", ... }, + { id: "...", part_code: "ITEM-002", ... } + ] +} +``` + +타임라인은 `selectedRows`에서 `linkedFilter.sourceField` 값을 추출하여, +자신의 데이터 중 `linkedFilter.targetField`가 일치하는 항목만 표시. diff --git a/docs/screen-implementation-guide/03_production/production-plan-test-scenario.md b/docs/screen-implementation-guide/03_production/production-plan-test-scenario.md new file mode 100644 index 00000000..538f9e1c --- /dev/null +++ b/docs/screen-implementation-guide/03_production/production-plan-test-scenario.md @@ -0,0 +1,451 @@ +# 생산계획 화면 (TOPSEAL_PP_MAIN) 테스트 시나리오 + +> **화면 URL**: `http://localhost:9771/screens/3985` +> **로그인 정보**: `topseal_admin` / `qlalfqjsgh11` +> **작성일**: 2026-03-16 + +--- + +## 사전 조건 + +- 백엔드 서버 (포트 8080) 실행 중 +- 프론트엔드 서버 (포트 9771) 실행 중 +- `topseal_admin` 계정으로 로그인 완료 +- 사이드바 > 생산관리 > 생산계획 메뉴 클릭하여 화면 진입 + +### 현재 테스트 데이터 현황 + +| 구분 | 건수 | 상세 | +|------|:----:|------| +| 완제품 생산계획 | 7건 | planned(3), in_progress(3), completed(1) | +| 반제품 생산계획 | 6건 | planned(2), in_progress(2), completed(1) | +| 설비(리소스) | 10개 | CNC밀링#1~#2, 머시닝센터#1, 레이저절단기, 프레스기#1, 용접기#1, 도장설비#1, 조립라인#1, 검사대#1~#2 | +| 수주 데이터 | 10건 | sales_order_mng | + +--- + +## TC-01. 화면 레이아웃 확인 + +### 목적 +화면이 설계대로 좌/우 분할 패널로 렌더링되는지 확인 + +### 테스트 단계 +1. 생산계획 화면 진입 +2. 좌측 패널에 "수주 데이터" 탭이 보이는지 확인 +3. 우측 패널에 "완제품" / "반제품" 탭이 보이는지 확인 +4. 분할 패널 비율이 약 45:55인지 확인 + +### 예상 결과 +- [ ] 좌측: "수주데이터" 탭 + "안전재고 부족분" 탭 +- [ ] 우측: "완제품" 탭 + "반제품" 탭 +- [ ] 하단에 버튼들 (새로고침, 자동 스케줄, 병합, 반제품계획, 저장) 표시 +- [ ] 좌측 하단에 "선택 품목 불러오기" 버튼 표시 + +--- + +## TC-02. 좌측 패널 - 수주데이터 그룹 테이블 + +### 목적 +v2-table-grouped 컴포넌트의 그룹화 및 접기/펼치기 기능 확인 + +### 테스트 단계 +1. "수주데이터" 탭 선택 +2. 데이터가 품목코드(part_code) 기준으로 그룹화되었는지 확인 +3. 그룹 헤더 행에 품명, 품목코드가 표시되는지 확인 +4. 그룹 헤더 클릭하여 접기/펼치기 토글 +5. "전체 펼치기" / "전체 접기" 버튼 동작 확인 +6. 그룹별 합계(수주량, 출고량, 잔량) 표시 확인 + +### 예상 결과 +- [ ] 데이터가 part_code 기준으로 그룹화되어 표시 +- [ ] 그룹 헤더에 `{품명} ({품목코드})` 형식으로 표시 +- [ ] 그룹 헤더 클릭 시 하위 행 접기/펼치기 동작 +- [ ] 전체 펼치기/접기 버튼 정상 동작 +- [ ] 그룹별 수주량/출고량/잔량 합계 표시 + +--- + +## TC-03. 좌측 패널 - 체크박스 선택 + +### 목적 +그룹 테이블에서 체크박스 선택이 정상 동작하는지 확인 + +### 테스트 단계 +1. 개별 행 체크박스 선택/해제 +2. 그룹 헤더 체크박스로 그룹 전체 선택/해제 +3. 다른 그룹의 행도 동시 선택 가능한지 확인 +4. 선택된 행이 하이라이트되는지 확인 + +### 예상 결과 +- [ ] 개별 행 체크박스 선택/해제 정상 +- [ ] 그룹 체크박스로 하위 전체 선택/해제 +- [ ] 여러 그룹에서 동시 선택 가능 +- [ ] 선택된 행 시각적 구분 (하이라이트) + +--- + +## TC-04. 우측 패널 - 완제품 타임라인 기본 표시 + +### 목적 +v2-timeline-scheduler의 기본 렌더링 및 데이터 표시 확인 + +### 테스트 단계 +1. "완제품" 탭 선택 (기본 선택) +2. 타임라인 헤더에 날짜가 표시되는지 확인 +3. 리소스(설비) 목록이 좌측에 표시되는지 확인 +4. 스케줄 바가 해당 설비/날짜에 표시되는지 확인 +5. 스케줄 바에 품명이 표시되는지 확인 +6. 오늘 날짜 라인(빨간 세로선)이 표시되는지 확인 + +### 예상 결과 +- [ ] 타임라인 헤더에 날짜 표시 (월 그룹 + 일별) +- [ ] 좌측 리소스 열에 설비명 표시 (프레스기#1, CNC밀링머신#1 등) +- [ ] 7건의 완제품 스케줄 바가 올바른 위치에 표시 +- [ ] 스케줄 바에 item_name 표시 +- [ ] 오늘 날짜 (2026-03-16) 위치에 빨간 세로선 표시 +- [ ] "반제품" 데이터는 보이지 않음 (staticFilters 적용 확인) + +--- + +## TC-05. 타임라인 - 상태별 색상 표시 + +### 목적 +스케줄 상태에 따른 색상 구분 확인 + +### 테스트 단계 +1. 완제품 탭에서 스케줄 바 색상 확인 +2. 각 상태별 색상이 다른지 확인 + +### 예상 결과 +- [ ] `planned` (계획): 파란색 (#3b82f6) +- [ ] `in_progress` (진행): 초록색 (#10b981) +- [ ] `completed` (완료): 회색 (#6b7280) +- [ ] `delayed` (지연): 빨간색 (#ef4444) - 해당 데이터 있으면 +- [ ] 상태별 색상이 명확히 구분됨 + +--- + +## TC-06. 타임라인 - 진행률 표시 + +### 목적 +스케줄 바 내부에 진행률이 시각적으로 표시되는지 확인 + +### 테스트 단계 +1. 진행률이 있는 스케줄 바 확인 +2. 바 내부에 진행률 비율만큼 채워진 영역 확인 +3. 진행률 퍼센트 텍스트 표시 확인 + +### 예상 결과 +- [ ] `탑씰 Type A` (id:103): 40% 진행률 표시 +- [ ] `탑씰 Type B` (id:2): 25% 진행률 표시 +- [ ] `탑씰 Type C` (id:105): 25% 진행률 표시 +- [ ] `탑씰 Type A` (id:4): 100% 진행률 표시 (완료) +- [ ] 바 내부에 진행 영역이 색이 다르게 채워짐 + +--- + +## TC-07. 타임라인 - 줌 레벨 전환 + +### 목적 +일/주/월 줌 레벨 전환이 정상 동작하는지 확인 + +### 테스트 단계 +1. 툴바에서 "주" (기본) 줌 레벨 확인 +2. "일" 줌 레벨로 전환 -> 날짜 간격 변화 확인 +3. "월" 줌 레벨로 전환 -> 날짜 간격 변화 확인 +4. 다시 "주" 줌 레벨로 복귀 + +### 예상 결과 +- [ ] "일" 모드: 날짜 셀이 넓어지고, 하루 단위로 상세 표시 +- [ ] "주" 모드: 기본 크기, 주 단위 표시 +- [ ] "월" 모드: 날짜 셀이 좁아지고, 월 단위로 축소 표시 +- [ ] 줌 레벨 전환 시 스케줄 바 위치/크기가 자동 조정 + +--- + +## TC-08. 타임라인 - 날짜 네비게이션 + +### 목적 +이전/다음/오늘 버튼으로 타임라인 이동이 정상 동작하는지 확인 + +### 테스트 단계 +1. 툴바에서 현재 표시 날짜 확인 +2. "다음" 버튼 클릭 -> 다음 주(또는 기간)로 이동 +3. "이전" 버튼 클릭 -> 이전 주로 이동 +4. "오늘" 버튼 클릭 -> 현재 날짜 영역으로 이동 +5. 2월 초 데이터가 있으므로 충분히 이전으로 이동하여 과거 데이터 확인 + +### 예상 결과 +- [ ] "다음" 클릭 시 타임라인이 오른쪽(미래)으로 이동 +- [ ] "이전" 클릭 시 타임라인이 왼쪽(과거)으로 이동 +- [ ] "오늘" 클릭 시 2026-03-16 부근으로 이동 +- [ ] 날짜 헤더의 표시 날짜가 변경됨 +- [ ] 이동 후에도 스케줄 바가 올바른 위치에 표시 + +--- + +## TC-09. 타임라인 - 드래그 이동 + +### 목적 +스케줄 바를 드래그하여 날짜를 변경하는 기능 확인 + +### 테스트 단계 +1. 완제품 탭에서 `planned` 상태의 스케줄 바 선택 (예: 탑씰 Type A, id:106) +2. 스케줄 바를 마우스로 클릭하고 좌/우로 드래그 +3. 드래그 중 바가 마우스를 따라 이동하는지 확인 (시각적 피드백) +4. 마우스 놓기 후 결과 확인 +5. 성공 시 토스트 알림 확인 +6. DB에 start_date/end_date가 변경되었는지 확인 + +### 예상 결과 +- [ ] 스케줄 바 드래그 시 시각적으로 이동 (opacity 변화) +- [ ] 드래그 완료 후 "스케줄이 이동되었습니다" 토스트 표시 +- [ ] 날짜가 드래그 거리만큼 변경 (시작일/종료일 동일 간격 유지) +- [ ] 실패 시 "스케줄 이동 실패" 에러 토스트 표시 후 원래 위치로 복귀 + +--- + +## TC-10. 타임라인 - 리사이즈 (기간 조정) + +### 목적 +스케줄 바의 시작/종료 핸들을 드래그하여 기간을 변경하는 기능 확인 + +### 테스트 단계 +1. 완제품 탭에서 스케줄 바에 마우스 호버 +2. 바 좌측/우측에 리사이즈 핸들이 나타나는지 확인 +3. 우측 핸들을 오른쪽으로 드래그 -> 종료일 연장 +4. 좌측 핸들을 오른쪽으로 드래그 -> 시작일 변경 +5. 성공 시 토스트 알림 확인 + +### 예상 결과 +- [ ] 바 호버 시 좌/우측에 리사이즈 핸들(세로 바) 표시 +- [ ] 우측 핸들 드래그 시 종료일만 변경 (시작일 유지) +- [ ] 좌측 핸들 드래그 시 시작일만 변경 (종료일 유지) +- [ ] 리사이즈 완료 후 "스케줄 기간이 변경되었습니다" 토스트 표시 +- [ ] 바 크기가 변경된 기간에 맞게 조정 + +--- + +## TC-11. 타임라인 - 충돌 감지 + +### 목적 +같은 설비에 시간이 겹치는 스케줄이 있을 때 충돌 표시가 되는지 확인 + +### 테스트 단계 +1. 충돌 데이터 확인: + - 프레스기#1 (equipment_id=11): id:103 (03/10~03/17), id:4 (01/28~01/30) → 겹치지 않아서 충돌 없음 + - 조립라인#1 (equipment_id=14): id:5 (02/01~02/02), id:6 (02/01~02/02) → 기간 겹침! (반제품) +2. 반제품 탭으로 이동하여 조립라인#1의 충돌 확인 +3. 또는 드래그로 충돌 상황을 만들어서 확인 + +### 예상 결과 +- [ ] 충돌 스케줄 바에 빨간 외곽선 (`ring-destructive`) 표시 +- [ ] 충돌 스케줄 바에 경고 아이콘 (AlertTriangle) 표시 +- [ ] 툴바에 "충돌 N건" 배지 표시 (빨간색) +- [ ] 충돌이 없는 경우 배지 미표시 + +--- + +## TC-12. 타임라인 - 범례 (Legend) + +### 목적 +하단 범례가 정상 표시되는지 확인 + +### 테스트 단계 +1. 타임라인 하단에 범례 영역이 표시되는지 확인 +2. 상태별 색상 스와치가 표시되는지 확인 +3. 마일스톤 아이콘이 표시되는지 확인 +4. 충돌 표시 범례가 표시되는지 확인 + +### 예상 결과 +- [ ] "계획" (파란색), "진행" (초록색), "완료" (회색), "지연" (빨간색), "취소" (연회색) 표시 +- [ ] "마일스톤" 다이아몬드 아이콘 표시 +- [ ] "충돌" 빨간 테두리 아이콘 표시 (showConflicts 설정 시) +- [ ] 범례가 타임라인 하단에 깔끔하게 배치 + +--- + +## TC-13. 반제품 탭 전환 + +### 목적 +반제품 탭으로 전환 시 반제품 데이터만 필터링되어 표시되는지 확인 (staticFilters) + +### 테스트 단계 +1. 우측 패널에서 "반제품" 탭 클릭 +2. 표시되는 스케줄이 반제품만인지 확인 +3. 완제품 데이터가 보이지 않는지 확인 +4. 다시 "완제품" 탭 클릭하여 전환 확인 + +### 예상 결과 +- [ ] "반제품" 탭 클릭 시 반제품 스케줄만 표시 (4건) +- [ ] 반제품 리소스: 조립라인#1, 용접기#1, 레이저절단기 +- [ ] 완제품 데이터는 표시되지 않음 +- [ ] "완제품" 탭 복귀 시 완제품 데이터만 표시 + +--- + +## TC-14. 버튼 - 새로고침 + +### 목적 +"새로고침" 버튼 클릭 시 데이터가 다시 로드되는지 확인 + +### 테스트 단계 +1. 우측 패널 하단의 "새로고침" 버튼 클릭 +2. 타임라인 데이터가 다시 로드되는지 확인 +3. 토스트 알림 확인 + +### 예상 결과 +- [ ] 클릭 시 API 호출 (GET /api/production/order-summary) +- [ ] 성공 시 "데이터를 새로고침했습니다." 토스트 표시 +- [ ] 타임라인 데이터 갱신 + +--- + +## TC-15. 버튼 - 자동 스케줄 + +### 목적 +좌측 테이블에서 수주 데이터를 선택한 후 자동 스케줄 생성이 되는지 확인 + +### 테스트 단계 +1. 좌측 패널에서 수주 데이터 행 1개 이상 체크박스 선택 +2. "자동 스케줄" 버튼 클릭 +3. 확인 다이얼로그 표시 확인 ("선택한 품목의 자동 스케줄을 생성하시겠습니까?") +4. "확인" 클릭 +5. 결과 확인 + +### 예상 결과 +- [ ] 확인 다이얼로그 표시 +- [ ] 성공 시 "자동 스케줄이 생성되었습니다." 토스트 표시 +- [ ] 우측 타임라인에 새로운 스케줄 바 추가 +- [ ] 실패 시 에러 메시지 표시 +- [ ] 선택 없이 클릭 시 적절한 안내 메시지 + +--- + +## TC-16. 버튼 - 선택 품목 불러오기 + +### 목적 +좌측 수주 데이터에서 선택한 품목을 생산계획으로 불러오는 기능 확인 + +### 테스트 단계 +1. 좌측 수주데이터 탭에서 품목 선택 (체크박스) +2. "선택 품목 불러오기" 버튼 클릭 +3. 확인 다이얼로그 ("선택한 품목의 생산계획을 생성하시겠습니까?") +4. 결과 확인 + +### 예상 결과 +- [ ] 확인 다이얼로그 표시 +- [ ] 성공 시 "선택 품목이 불러와졌습니다." 토스트 표시 +- [ ] 타임라인 자동 새로고침 + +--- + +## TC-17. 버튼 - 저장 + +### 목적 +변경된 생산계획 데이터가 저장되는지 확인 + +### 테스트 단계 +1. 타임라인에서 스케줄 바 드래그 또는 리사이즈로 데이터 변경 +2. "저장" 버튼 클릭 +3. 저장 결과 확인 + +### 예상 결과 +- [ ] 성공 시 "생산계획이 저장되었습니다." 토스트 표시 +- [ ] 변경 사항이 DB에 반영 + +--- + +## TC-18. 반응형 CSS 확인 + +### 목적 +공통 반응형 CSS가 올바르게 적용되었는지 확인 + +### 테스트 단계 +1. 브라우저 창 너비를 640px 이하로 줄이기 (모바일) +2. 텍스트 크기, 버튼 크기, 패딩 변화 확인 +3. 브라우저 창 너비를 1280px 이상으로 늘리기 (데스크톱) +4. 원래 크기로 복귀 확인 + +### 예상 결과 +- [ ] 모바일(~640px): 텍스트 `text-[10px]`, 작은 버튼, 좁은 패딩 +- [ ] 데스크톱(640px~): 텍스트 `text-sm`, 기본 버튼, 넓은 패딩 +- [ ] 줌 버튼, 네비게이션 버튼, 리소스명, 날짜 헤더 모두 반응형 적용 +- [ ] 스케줄 바 내부 텍스트도 반응형 (text-[10px] sm:text-xs) +- [ ] 범례 텍스트도 반응형 + +--- + +## TC-19. 마일스톤 표시 + +### 목적 +시작일과 종료일이 같은 스케줄이 마일스톤(다이아몬드)으로 표시되는지 확인 + +### 테스트 단계 +1. DB에 마일스톤 테스트 데이터 추가: + ```sql + INSERT INTO production_plan_mng (id, item_name, product_type, status, start_date, end_date, equipment_id, progress_rate, company_code) + VALUES (200, '마일스톤 테스트', '완제품', 'planned', '2026-03-20', '2026-03-20', 9, '0', 'COMPANY_7'); + ``` +2. 새로고침 후 해당 날짜에 다이아몬드 마커가 표시되는지 확인 +3. 호버 시 정보 표시 확인 + +### 예상 결과 +- [ ] 시작일 = 종료일인 스케줄은 바 대신 다이아몬드 마커로 표시 +- [ ] 다이아몬드가 45도 회전된 정사각형으로 표시 +- [ ] 호버 시 효과 적용 + +--- + +## TC-20. 안전재고 부족분 탭 + +### 목적 +좌측 패널의 "안전재고 부족분" 탭이 정상 동작하는지 확인 + +### 테스트 단계 +1. 좌측 패널에서 "안전재고 부족분" 탭 클릭 +2. inventory_stock 테이블 데이터가 표시되는지 확인 +3. 빈 데이터인 경우 빈 상태 메시지 확인 + +### 예상 결과 +- [ ] 탭 전환 정상 동작 +- [ ] 데이터 있으면: 품목코드, 현재고, 안전재고, 창고, 최근입고일 표시 +- [ ] 데이터 없으면: "안전재고 부족분 데이터가 없습니다" 메시지 + +--- + +## 알려진 이슈 / 참고 사항 + +| 번호 | 내용 | 심각도 | +|:----:|------|:------:| +| 1 | "1 Issue" 배지가 화면 좌측 하단에 표시됨 (원인 미확인) | 낮음 | +| 2 | 생산계획 화면 URL 직접 접근 시 회사정보 화면(138)이 먼저 보일 수 있음 → 사이드바 메뉴를 통해 접근 권장 | 중간 | +| 3 | 설비(equipment_info)의 equipment_group이 null → 리소스 그룹핑 미표시 | 낮음 | +| 4 | 가상 스크롤은 리소스(설비) 30개 이상일 때 자동 활성화 (현재 10개라 비활성) | 참고 | + +--- + +## 테스트 결과 요약 + +| TC | 항목 | 결과 | 비고 | +|:--:|------|:----:|------| +| 01 | 화면 레이아웃 | | | +| 02 | 수주데이터 그룹 테이블 | | | +| 03 | 체크박스 선택 | | | +| 04 | 완제품 타임라인 기본 표시 | | | +| 05 | 상태별 색상 | | | +| 06 | 진행률 표시 | | | +| 07 | 줌 레벨 전환 | | | +| 08 | 날짜 네비게이션 | | | +| 09 | 드래그 이동 | | | +| 10 | 리사이즈 | | | +| 11 | 충돌 감지 | | | +| 12 | 범례 | | | +| 13 | 반제품 탭 전환 | | | +| 14 | 새로고침 버튼 | | | +| 15 | 자동 스케줄 버튼 | | | +| 16 | 선택 품목 불러오기 | | | +| 17 | 저장 버튼 | | | +| 18 | 반응형 CSS | | | +| 19 | 마일스톤 표시 | | | +| 20 | 안전재고 부족분 탭 | | | diff --git a/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts b/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts index d9f40aca..e2f415e7 100644 --- a/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts +++ b/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts @@ -153,15 +153,37 @@ export function useGroupedData( } ); - const responseData = response.data?.data?.data || response.data?.data || []; - setRawData(Array.isArray(responseData) ? responseData : []); + let responseData = response.data?.data?.data || response.data?.data || []; + responseData = Array.isArray(responseData) ? responseData : []; + + // dataFilter 적용 (클라이언트 사이드 필터링) + if (config.dataFilter && config.dataFilter.length > 0) { + responseData = responseData.filter((item: any) => { + return config.dataFilter!.every((f) => { + const val = item[f.column]; + switch (f.operator) { + case "eq": return val === f.value; + case "ne": return f.value === null ? (val !== null && val !== undefined && val !== "") : val !== f.value; + case "gt": return Number(val) > Number(f.value); + case "lt": return Number(val) < Number(f.value); + case "gte": return Number(val) >= Number(f.value); + case "lte": return Number(val) <= Number(f.value); + case "like": return String(val ?? "").includes(String(f.value)); + case "in": return Array.isArray(f.value) ? f.value.includes(val) : false; + default: return true; + } + }); + }); + } + + setRawData(responseData); } catch (err: any) { setError(err.message || "데이터 로드 중 오류 발생"); setRawData([]); } finally { setIsLoading(false); } - }, [tableName, externalData, searchFilters]); + }, [tableName, externalData, searchFilters, config.dataFilter]); // 초기 데이터 로드 useEffect(() => { diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx index 354869bc..3a69da65 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useMemo, useRef } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, @@ -9,18 +9,27 @@ import { Loader2, ZoomIn, ZoomOut, + Package, + Zap, + RefreshCw, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { TimelineSchedulerComponentProps, ScheduleItem, ZoomLevel, } from "./types"; import { useTimelineData } from "./hooks/useTimelineData"; -import { TimelineHeader, ResourceRow, TimelineLegend } from "./components"; -import { zoomLevelOptions, defaultTimelineSchedulerConfig } from "./config"; +import { TimelineHeader, ResourceRow, TimelineLegend, ItemTimelineCard, groupSchedulesByItem, SchedulePreviewDialog } from "./components"; +import { zoomLevelOptions, defaultTimelineSchedulerConfig, statusOptions } from "./config"; import { detectConflicts, addDaysToDateString } from "./utils/conflictDetection"; +import { v2EventBus, V2_EVENTS } from "@/lib/v2-core"; +import { apiClient } from "@/lib/api/client"; + +// 가상 스크롤 활성화 임계값 (리소스 수) +const VIRTUAL_THRESHOLD = 30; /** * v2-timeline-scheduler 메인 컴포넌트 @@ -44,9 +53,59 @@ export function TimelineSchedulerComponent({ }: TimelineSchedulerComponentProps) { const containerRef = useRef(null); + // ────────── 자동 스케줄 생성 상태 ────────── + const [showPreviewDialog, setShowPreviewDialog] = useState(false); + const [previewLoading, setPreviewLoading] = useState(false); + const [previewApplying, setPreviewApplying] = useState(false); + const [previewSummary, setPreviewSummary] = useState(null); + const [previewItems, setPreviewItems] = useState([]); + const [previewDeleted, setPreviewDeleted] = useState([]); + const [previewKept, setPreviewKept] = useState([]); + const linkedFilterValuesRef = useRef([]); + + // ────────── 반제품 계획 생성 상태 ────────── + const [showSemiPreviewDialog, setShowSemiPreviewDialog] = useState(false); + const [semiPreviewLoading, setSemiPreviewLoading] = useState(false); + const [semiPreviewApplying, setSemiPreviewApplying] = useState(false); + const [semiPreviewSummary, setSemiPreviewSummary] = useState(null); + const [semiPreviewItems, setSemiPreviewItems] = useState([]); + const [semiPreviewDeleted, setSemiPreviewDeleted] = useState([]); + const [semiPreviewKept, setSemiPreviewKept] = useState([]); + + // ────────── linkedFilter 상태 ────────── + const linkedFilter = config.linkedFilter; + const hasLinkedFilter = !!linkedFilter; + const [linkedFilterValues, setLinkedFilterValues] = useState([]); + const [hasReceivedSelection, setHasReceivedSelection] = useState(false); + + // linkedFilter 이벤트 수신 + useEffect(() => { + if (!hasLinkedFilter) return; + + const handler = (event: any) => { + if (linkedFilter!.sourceTableName && event.tableName !== linkedFilter!.sourceTableName) return; + if (linkedFilter!.sourceComponentId && event.componentId !== linkedFilter!.sourceComponentId) return; + + const selectedRows: any[] = event.selectedRows || []; + const sourceField = linkedFilter!.sourceField; + + const values = selectedRows + .map((row: any) => String(row[sourceField] ?? "")) + .filter((v: string) => v !== "" && v !== "undefined" && v !== "null"); + + const uniqueValues = [...new Set(values)]; + setLinkedFilterValues(uniqueValues); + setHasReceivedSelection(true); + linkedFilterValuesRef.current = selectedRows; + }; + + const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, handler); + return unsubscribe; + }, [hasLinkedFilter, linkedFilter]); + // 타임라인 데이터 훅 const { - schedules, + schedules: rawSchedules, resources, isLoading: hookLoading, error: hookError, @@ -58,8 +117,21 @@ export function TimelineSchedulerComponent({ goToNext, goToToday, updateSchedule, + refresh: refreshTimeline, } = useTimelineData(config, externalSchedules, externalResources); + // linkedFilter 적용: 선택된 값으로 스케줄 필터링 + const schedules = useMemo(() => { + if (!hasLinkedFilter) return rawSchedules; + if (linkedFilterValues.length === 0) return []; + + const targetField = linkedFilter!.targetField; + return rawSchedules.filter((s) => { + const val = String((s.data as any)?.[targetField] ?? (s as any)[targetField] ?? ""); + return linkedFilterValues.includes(val); + }); + }, [rawSchedules, hasLinkedFilter, linkedFilterValues, linkedFilter]); + const isLoading = externalLoading ?? hookLoading; const error = externalError ?? hookError; @@ -267,11 +339,212 @@ export function TimelineSchedulerComponent({ } }, [onAddSchedule, effectiveResources]); + // ────────── 자동 스케줄 생성: 미리보기 요청 ────────── + const handleAutoSchedulePreview = useCallback(async () => { + const selectedRows = linkedFilterValuesRef.current; + if (!selectedRows || selectedRows.length === 0) { + toast.warning("좌측에서 품목을 선택해주세요"); + return; + } + + const sourceField = config.linkedFilter?.sourceField || "part_code"; + const grouped = new Map(); + selectedRows.forEach((row: any) => { + const key = row[sourceField] || ""; + if (!key) return; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(row); + }); + + const items = Array.from(grouped.entries()).map(([itemCode, rows]) => { + const totalBalanceQty = rows.reduce((sum: number, r: any) => sum + (Number(r.balance_qty) || 0), 0); + const earliestDueDate = rows + .map((r: any) => r.due_date) + .filter(Boolean) + .sort()[0] || new Date().toISOString().split("T")[0]; + const first = rows[0]; + + return { + item_code: itemCode, + item_name: first.part_name || first.item_name || itemCode, + required_qty: totalBalanceQty, + earliest_due_date: typeof earliestDueDate === "string" ? earliestDueDate.split("T")[0] : earliestDueDate, + hourly_capacity: Number(first.hourly_capacity) || undefined, + daily_capacity: Number(first.daily_capacity) || undefined, + }; + }).filter((item) => item.required_qty > 0); + + if (items.length === 0) { + toast.warning("선택된 품목의 잔량이 없습니다"); + return; + } + + setShowPreviewDialog(true); + setPreviewLoading(true); + + try { + const response = await apiClient.post("/production/generate-schedule/preview", { + items, + options: { + product_type: config.staticFilters?.product_type || "완제품", + safety_lead_time: 1, + recalculate_unstarted: true, + }, + }); + + if (response.data?.success) { + setPreviewSummary(response.data.data.summary); + setPreviewItems(response.data.data.previews); + setPreviewDeleted(response.data.data.deletedSchedules || []); + setPreviewKept(response.data.data.keptSchedules || []); + } else { + toast.error("미리보기 생성 실패"); + setShowPreviewDialog(false); + } + } catch (err: any) { + toast.error("미리보기 요청 실패", { description: err.message }); + setShowPreviewDialog(false); + } finally { + setPreviewLoading(false); + } + }, [config.linkedFilter, config.staticFilters]); + + // ────────── 자동 스케줄 생성: 확인 및 적용 ────────── + const handleAutoScheduleApply = useCallback(async () => { + if (!previewItems || previewItems.length === 0) return; + + setPreviewApplying(true); + + const items = previewItems.map((p: any) => ({ + item_code: p.item_code, + item_name: p.item_name, + required_qty: p.required_qty, + earliest_due_date: p.due_date, + hourly_capacity: p.hourly_capacity, + daily_capacity: p.daily_capacity, + })); + + try { + const response = await apiClient.post("/production/generate-schedule", { + items, + options: { + product_type: config.staticFilters?.product_type || "완제품", + safety_lead_time: 1, + recalculate_unstarted: true, + }, + }); + + if (response.data?.success) { + const summary = response.data.data.summary; + toast.success("생산계획 업데이트 완료", { + description: `신규: ${summary.new_count}건, 유지: ${summary.kept_count}건, 삭제: ${summary.deleted_count}건`, + }); + setShowPreviewDialog(false); + refreshTimeline(); + } else { + toast.error("생산계획 생성 실패"); + } + } catch (err: any) { + toast.error("생산계획 생성 실패", { description: err.message }); + } finally { + setPreviewApplying(false); + } + }, [previewItems, config.staticFilters, refreshTimeline]); + + // ────────── 반제품 계획 생성: 미리보기 요청 ────────── + const handleSemiSchedulePreview = useCallback(async () => { + // 현재 타임라인에 표시된 완제품 스케줄의 plan ID 수집 + const finishedSchedules = schedules.filter((s) => { + const productType = (s.data as any)?.product_type || ""; + return productType === "완제품"; + }); + + if (finishedSchedules.length === 0) { + toast.warning("완제품 스케줄이 없습니다. 먼저 완제품 계획을 생성해주세요."); + return; + } + + const planIds = finishedSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id)); + if (planIds.length === 0) { + toast.warning("유효한 완제품 계획 ID가 없습니다"); + return; + } + + setShowSemiPreviewDialog(true); + setSemiPreviewLoading(true); + + try { + const response = await apiClient.post("/production/generate-semi-schedule/preview", { + plan_ids: planIds, + options: { considerStock: true }, + }); + + if (response.data?.success) { + setSemiPreviewSummary(response.data.data.summary); + setSemiPreviewItems(response.data.data.previews || []); + setSemiPreviewDeleted(response.data.data.deletedSchedules || []); + setSemiPreviewKept(response.data.data.keptSchedules || []); + } else { + toast.error("반제품 미리보기 실패", { description: response.data?.message }); + setShowSemiPreviewDialog(false); + } + } catch (err: any) { + toast.error("반제품 미리보기 요청 실패", { description: err.message }); + setShowSemiPreviewDialog(false); + } finally { + setSemiPreviewLoading(false); + } + }, [schedules]); + + // ────────── 반제품 계획 생성: 확인 및 적용 ────────── + const handleSemiScheduleApply = useCallback(async () => { + const finishedSchedules = schedules.filter((s) => { + const productType = (s.data as any)?.product_type || ""; + return productType === "완제품"; + }); + const planIds = finishedSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id)); + + if (planIds.length === 0) return; + + setSemiPreviewApplying(true); + + try { + const response = await apiClient.post("/production/generate-semi-schedule", { + plan_ids: planIds, + options: { considerStock: true }, + }); + + if (response.data?.success) { + const data = response.data.data; + toast.success("반제품 계획 생성 완료", { + description: `${data.count}건의 반제품 계획이 생성되었습니다`, + }); + setShowSemiPreviewDialog(false); + refreshTimeline(); + } else { + toast.error("반제품 계획 생성 실패"); + } + } catch (err: any) { + toast.error("반제품 계획 생성 실패", { description: err.message }); + } finally { + setSemiPreviewApplying(false); + } + }, [schedules, refreshTimeline]); + // ────────── 하단 영역 높이 계산 (툴바 + 범례) ────────── const showToolbar = config.showToolbar !== false; const showLegend = config.showLegend !== false; - const toolbarHeight = showToolbar ? 36 : 0; - const legendHeight = showLegend ? 28 : 0; + + // ────────── 가상 스크롤 ────────── + const scrollContainerRef = useRef(null); + const useVirtual = effectiveResources.length >= VIRTUAL_THRESHOLD; + + const virtualizer = useVirtualizer({ + count: effectiveResources.length, + getScrollElement: () => scrollContainerRef.current, + estimateSize: () => rowHeight, + overscan: 5, + }); // ────────── 디자인 모드 플레이스홀더 ────────── if (isDesignMode) { @@ -320,8 +593,49 @@ export function TimelineSchedulerComponent({ ); } - // ────────── 데이터 없음 ────────── - if (schedules.length === 0) { + // ────────── linkedFilter 빈 상태 (itemGrouped가 아닌 경우만 early return) ────────── + // itemGrouped 모드에서는 툴바를 항상 보여주기 위해 여기서 return하지 않음 + if (config.viewMode !== "itemGrouped") { + if (hasLinkedFilter && !hasReceivedSelection) { + const emptyMsg = linkedFilter?.emptyMessage || "좌측 목록에서 품목 또는 수주를 선택하세요"; + return ( +
+
+ +

{emptyMsg}

+

+ 선택한 항목에 대한 생산계획 타임라인이 여기에 표시됩니다 +

+
+
+ ); + } + + if (hasLinkedFilter && hasReceivedSelection && schedules.length === 0) { + return ( +
+
+ +

+ 선택한 항목에 대한 스케줄이 없습니다 +

+

+ 다른 품목을 선택하거나 스케줄을 생성해 주세요 +

+
+
+ ); + } + } + + // ────────── 데이터 없음 (linkedFilter 없고 itemGrouped가 아닌 경우) ────────── + if (schedules.length === 0 && config.viewMode !== "itemGrouped") { return (
+ {/* 툴바: 액션 버튼 + 네비게이션 */} + {showToolbar && ( +
+ {/* 네비게이션 */} +
+ {config.showNavigation !== false && ( + <> + + + + + )} + + {viewStartDate.toLocaleDateString("ko-KR")} ~ {viewEndDate.toLocaleDateString("ko-KR")} + +
+ + {/* 줌 + 액션 버튼 */} +
+ {config.showZoomControls !== false && ( + <> + + + {zoomLevelOptions.find((o) => o.value === zoomLevel)?.label} + + +
+ + )} + + + {config.staticFilters?.product_type === "완제품" && ( + <> + + + + )} +
+
+ )} + + {/* 범례 */} + {showLegend && ( +
+ 생산 상태: + {statusOptions.map((s) => ( +
+
+ {s.label} +
+ ))} + 납기: +
+
+ 납기일 +
+
+ )} + + {/* 품목별 카드 목록 또는 빈 상태 */} + {itemGroups.length > 0 ? ( +
+ {itemGroups.map((group) => ( + + ))} +
+ ) : ( +
+
+ {hasLinkedFilter && !hasReceivedSelection ? ( + <> + +

+ {linkedFilter?.emptyMessage || "좌측 목록에서 품목을 선택하세요"} +

+

+ 선택한 항목에 대한 생산계획 타임라인이 여기에 표시됩니다 +

+ + ) : hasLinkedFilter && hasReceivedSelection ? ( + <> + +

+ 선택한 항목에 대한 스케줄이 없습니다 +

+

+ 위 "자동 스케줄 생성" 버튼으로 생산계획을 생성하세요 +

+ + ) : ( + <> + +

스케줄 데이터가 없습니다

+ + )} +
+
+ )} + + {/* 완제품 스케줄 생성 미리보기 다이얼로그 */} + + + {/* 반제품 계획 생성 미리보기 다이얼로그 */} + +
+ ); + } + + // ────────── 메인 렌더링 (리소스 기반) ────────── return (
+
{/* 헤더 */} - {/* 리소스 행들 */} -
- {effectiveResources.map((resource) => ( - - ))} -
+ {/* 리소스 행들 - 30개 이상이면 가상 스크롤 */} + {useVirtual ? ( +
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const resource = effectiveResources[virtualRow.index]; + return ( +
+ +
+ ); + })} +
+ ) : ( +
+ {effectiveResources.map((resource) => ( + + ))} +
+ )}
diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/ItemTimelineCard.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/ItemTimelineCard.tsx new file mode 100644 index 00000000..01e72a1c --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/ItemTimelineCard.tsx @@ -0,0 +1,297 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { cn } from "@/lib/utils"; +import { Flame } from "lucide-react"; +import { ScheduleItem, TimelineSchedulerConfig, ZoomLevel } from "../types"; +import { statusOptions, dayLabels } from "../config"; + +interface ItemScheduleGroup { + itemCode: string; + itemName: string; + hourlyCapacity: number; + dailyCapacity: number; + schedules: ScheduleItem[]; + totalPlanQty: number; + totalCompletedQty: number; + remainingQty: number; + dueDates: { date: string; isUrgent: boolean }[]; +} + +interface ItemTimelineCardProps { + group: ItemScheduleGroup; + viewStartDate: Date; + viewEndDate: Date; + zoomLevel: ZoomLevel; + cellWidth: number; + config: TimelineSchedulerConfig; + onScheduleClick?: (schedule: ScheduleItem) => void; +} + +const toDateString = (d: Date) => d.toISOString().split("T")[0]; + +const addDays = (d: Date, n: number) => { + const r = new Date(d); + r.setDate(r.getDate() + n); + return r; +}; + +const diffDays = (a: Date, b: Date) => + Math.round((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24)); + +function generateDateCells(start: Date, end: Date) { + const cells: { date: Date; label: string; dayLabel: string; isWeekend: boolean; isToday: boolean; dateStr: string }[] = []; + const today = toDateString(new Date()); + let cur = new Date(start); + while (cur <= end) { + const d = new Date(cur); + const dow = d.getDay(); + cells.push({ + date: d, + label: String(d.getDate()), + dayLabel: dayLabels[dow], + isWeekend: dow === 0 || dow === 6, + isToday: toDateString(d) === today, + dateStr: toDateString(d), + }); + cur = addDays(cur, 1); + } + return cells; +} + +export function ItemTimelineCard({ + group, + viewStartDate, + viewEndDate, + zoomLevel, + cellWidth, + config, + onScheduleClick, +}: ItemTimelineCardProps) { + const scrollRef = useRef(null); + + const dateCells = useMemo( + () => generateDateCells(viewStartDate, viewEndDate), + [viewStartDate, viewEndDate] + ); + + const totalWidth = dateCells.length * cellWidth; + + const dueDateSet = useMemo(() => { + const set = new Set(); + group.dueDates.forEach((d) => set.add(d.date)); + return set; + }, [group.dueDates]); + + const urgentDateSet = useMemo(() => { + const set = new Set(); + group.dueDates.filter((d) => d.isUrgent).forEach((d) => set.add(d.date)); + return set; + }, [group.dueDates]); + + const statusColor = (status: string) => + config.statusColors?.[status as keyof typeof config.statusColors] || + statusOptions.find((s) => s.value === status)?.color || + "#3b82f6"; + + const isUrgentItem = group.dueDates.some((d) => d.isUrgent); + const hasRemaining = group.remainingQty > 0; + + return ( +
+ {/* 품목 헤더 */} +
+
+ +
+

{group.itemCode}

+

{group.itemName}

+
+
+
+

+ 시간당: {group.hourlyCapacity.toLocaleString()} EA +

+

+ 일일: {group.dailyCapacity.toLocaleString()} EA +

+
+
+ + {/* 타임라인 영역 */} +
+
+ {/* 날짜 헤더 */} +
+ {dateCells.map((cell) => { + const isDueDate = dueDateSet.has(cell.dateStr); + const isUrgentDate = urgentDateSet.has(cell.dateStr); + return ( +
+ + {cell.label} + + + {cell.dayLabel} + +
+ ); + })} +
+ + {/* 스케줄 바 영역 */} +
+ {group.schedules.map((schedule) => { + const schedStart = new Date(schedule.startDate); + const schedEnd = new Date(schedule.endDate); + + const startOffset = diffDays(schedStart, viewStartDate); + const endOffset = diffDays(schedEnd, viewStartDate); + + const left = Math.max(0, startOffset * cellWidth); + const right = Math.min(totalWidth, (endOffset + 1) * cellWidth); + const width = Math.max(cellWidth * 0.5, right - left); + + if (right < 0 || left > totalWidth) return null; + + const qty = Number(schedule.data?.plan_qty) || 0; + const color = statusColor(schedule.status); + + return ( +
onScheduleClick?.(schedule)} + title={`${schedule.title} (${schedule.startDate} ~ ${schedule.endDate})`} + > +
+ {qty > 0 ? `${qty.toLocaleString()} EA` : schedule.title} +
+
+ ); + })} + + {/* 납기일 마커 */} + {group.dueDates.map((dueDate, idx) => { + const d = new Date(dueDate.date); + const offset = diffDays(d, viewStartDate); + if (offset < 0 || offset > dateCells.length) return null; + const left = offset * cellWidth + cellWidth / 2; + return ( +
+
+
+ ); + })} +
+
+
+ + {/* 하단 잔량 영역 */} +
+ + {hasRemaining && ( +
+ {isUrgentItem && } + {group.remainingQty.toLocaleString()} EA +
+ )} + {/* 스크롤 인디케이터 */} +
+
+
+
+
+ ); +} + +/** + * 스케줄 데이터를 품목별로 그룹화 + */ +export function groupSchedulesByItem(schedules: ScheduleItem[]): ItemScheduleGroup[] { + const grouped = new Map(); + + schedules.forEach((s) => { + const key = s.data?.item_code || "unknown"; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(s); + }); + + const result: ItemScheduleGroup[] = []; + + grouped.forEach((items, itemCode) => { + const first = items[0]; + const hourlyCapacity = Number(first.data?.hourly_capacity) || 0; + const dailyCapacity = Number(first.data?.daily_capacity) || 0; + const totalPlanQty = items.reduce((sum, s) => sum + (Number(s.data?.plan_qty) || 0), 0); + const totalCompletedQty = items.reduce((sum, s) => sum + (Number(s.data?.completed_qty) || 0), 0); + + const dueDates: { date: string; isUrgent: boolean }[] = []; + const seenDueDates = new Set(); + items.forEach((s) => { + const dd = s.data?.due_date; + if (dd) { + const dateStr = typeof dd === "string" ? dd.split("T")[0] : ""; + if (dateStr && !seenDueDates.has(dateStr)) { + seenDueDates.add(dateStr); + const isUrgent = s.data?.priority === "urgent" || s.data?.priority === "high"; + dueDates.push({ date: dateStr, isUrgent }); + } + } + }); + + result.push({ + itemCode, + itemName: first.data?.item_name || first.title || itemCode, + hourlyCapacity, + dailyCapacity, + schedules: items.sort( + (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ), + totalPlanQty, + totalCompletedQty, + remainingQty: totalPlanQty - totalCompletedQty, + dueDates: dueDates.sort((a, b) => a.date.localeCompare(b.date)), + }); + }); + + return result.sort((a, b) => a.itemCode.localeCompare(b.itemCode)); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/SchedulePreviewDialog.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/components/SchedulePreviewDialog.tsx new file mode 100644 index 00000000..ab130659 --- /dev/null +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/SchedulePreviewDialog.tsx @@ -0,0 +1,282 @@ +"use client"; + +import React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Loader2, AlertTriangle, Check, X, Trash2, Play } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { statusOptions } from "../config"; + +interface PreviewItem { + item_code: string; + item_name: string; + required_qty: number; + daily_capacity: number; + hourly_capacity: number; + production_days: number; + start_date: string; + end_date: string; + due_date: string; + order_count: number; + status: string; +} + +interface ExistingSchedule { + id: string; + plan_no: string; + item_code: string; + item_name: string; + plan_qty: string; + start_date: string; + end_date: string; + status: string; + completed_qty?: string; +} + +interface PreviewSummary { + total: number; + new_count: number; + kept_count: number; + deleted_count: number; +} + +interface SchedulePreviewDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + isLoading: boolean; + summary: PreviewSummary | null; + previews: PreviewItem[]; + deletedSchedules: ExistingSchedule[]; + keptSchedules: ExistingSchedule[]; + onConfirm: () => void; + isApplying: boolean; + title?: string; + description?: string; +} + +const summaryCards = [ + { key: "total", label: "총 계획", color: "bg-primary/10 text-primary" }, + { key: "new_count", label: "신규 입력", color: "bg-emerald-50 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400" }, + { key: "deleted_count", label: "삭제될", color: "bg-destructive/10 text-destructive" }, + { key: "kept_count", label: "유지(진행중)", color: "bg-amber-50 text-amber-600 dark:bg-amber-950 dark:text-amber-400" }, +]; + +function formatDate(d: string | null | undefined): string { + if (!d) return "-"; + const s = typeof d === "string" ? d : String(d); + return s.split("T")[0]; +} + +export function SchedulePreviewDialog({ + open, + onOpenChange, + isLoading, + summary, + previews, + deletedSchedules, + keptSchedules, + onConfirm, + isApplying, + title, + description, +}: SchedulePreviewDialogProps) { + return ( + + + + + {title || "생산계획 변경사항 확인"} + + + {description || "변경사항을 확인해주세요"} + + + + {isLoading ? ( +
+ + 미리보기 생성 중... +
+ ) : summary ? ( +
+ {/* 경고 배너 */} +
+ +
+

변경사항을 확인해주세요

+

+ 아래 변경사항을 검토하신 후 확인 버튼을 눌러주시면 생산계획이 업데이트됩니다. +

+
+
+ + {/* 요약 카드 */} +
+ {summaryCards.map((card) => ( +
+

+ {(summary as any)[card.key] ?? 0} +

+

{card.label}

+
+ ))} +
+ + {/* 신규 생성 목록 */} + {previews.length > 0 && ( +
+

+ + 신규 생성되는 계획 ({previews.length}건) +

+
+ {previews.map((item, idx) => { + const statusInfo = statusOptions.find((s) => s.value === item.status); + return ( +
+
+

+ {item.item_code} - {item.item_name} +

+ + {statusInfo?.label || item.status} + +
+

+ 수량: {(item.required_qty || (item as any).plan_qty || 0).toLocaleString()} EA +

+
+ 시작일: {formatDate(item.start_date)} + 종료일: {formatDate(item.end_date)} +
+ {item.order_count ? ( +

+ {item.order_count}건 수주 통합 (총 {item.required_qty.toLocaleString()} EA) +

+ ) : (item as any).parent_item_name ? ( +

+ 상위: {(item as any).parent_plan_no} ({(item as any).parent_item_name}) | BOM 수량: {(item as any).bom_qty || 1} +

+ ) : null} +
+ ); + })} +
+
+ )} + + {/* 삭제될 목록 */} + {deletedSchedules.length > 0 && ( +
+

+ + 삭제될 기존 계획 ({deletedSchedules.length}건) +

+
+ {deletedSchedules.map((item, idx) => ( +
+
+

+ {item.item_code} - {item.item_name} +

+ + 삭제 예정 + +
+

+ {item.plan_no} | 수량: {Number(item.plan_qty || 0).toLocaleString()} EA +

+
+ 시작일: {formatDate(item.start_date)} + 종료일: {formatDate(item.end_date)} +
+
+ ))} +
+
+ )} + + {/* 유지될 목록 (진행중) */} + {keptSchedules.length > 0 && ( +
+

+ + 유지되는 진행중 계획 ({keptSchedules.length}건) +

+
+ {keptSchedules.map((item, idx) => { + const statusInfo = statusOptions.find((s) => s.value === item.status); + return ( +
+
+

+ {item.item_code} - {item.item_name} +

+ + {statusInfo?.label || item.status} + +
+

+ {item.plan_no} | 수량: {Number(item.plan_qty || 0).toLocaleString()} EA + {item.completed_qty ? ` (완료: ${Number(item.completed_qty).toLocaleString()} EA)` : ""} +

+
+ 시작일: {formatDate(item.start_date)} + 종료일: {formatDate(item.end_date)} +
+
+ ); + })} +
+
+ )} +
+ ) : ( +
+ 미리보기 데이터를 불러올 수 없습니다 +
+ )} + + + + + +
+
+ ); +} diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts index 4ac2af4b..e9064267 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/components/index.ts @@ -2,3 +2,5 @@ export { TimelineHeader } from "./TimelineHeader"; export { ScheduleBar } from "./ScheduleBar"; export { ResourceRow } from "./ResourceRow"; export { TimelineLegend } from "./TimelineLegend"; +export { ItemTimelineCard, groupSchedulesByItem } from "./ItemTimelineCard"; +export { SchedulePreviewDialog } from "./SchedulePreviewDialog"; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts index afcc9f5e..aa5c4edd 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -225,6 +225,35 @@ export interface TimelineSchedulerConfig extends ComponentConfig { /** 최대 높이 */ maxHeight?: number | string; + + /** + * 표시 모드 + * - "resource": 기존 설비(리소스) 기반 간트 차트 (기본값) + * - "itemGrouped": 품목별 카드형 타임라인 (참고 이미지 스타일) + */ + viewMode?: "resource" | "itemGrouped"; + + /** 범례 표시 여부 */ + showLegend?: boolean; + + /** + * 연결 필터 설정: 다른 컴포넌트의 선택에 따라 데이터를 필터링 + * 설정 시 초기 상태는 빈 화면, 선택 이벤트 수신 시 필터링된 데이터 표시 + */ + linkedFilter?: { + /** 소스 컴포넌트 ID (선택 이벤트를 발생시키는 컴포넌트) */ + sourceComponentId?: string; + /** 소스 테이블명 (이벤트의 tableName과 매칭) */ + sourceTableName?: string; + /** 소스 필드 (선택된 행에서 추출할 필드) */ + sourceField: string; + /** 타겟 필드 (타임라인 데이터에서 필터링할 필드) */ + targetField: string; + /** 선택 없을 때 빈 상태 표시 여부 (기본: true) */ + showEmptyWhenNoSelection?: boolean; + /** 빈 상태 메시지 */ + emptyMessage?: string; + }; } /** diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f38af595..230d3139 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,6 +35,7 @@ "@react-three/fiber": "^9.4.0", "@tanstack/react-query": "^5.86.0", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.22", "@tiptap/core": "^2.27.1", "@tiptap/extension-placeholder": "^2.27.1", "@tiptap/pm": "^2.27.1", @@ -3756,6 +3757,23 @@ "react-dom": ">=16.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.22", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.22.tgz", + "integrity": "sha512-EaOrBBJLi3M0bTMQRjGkxLXRw7Gizwntoy5E2Q2UnSbML7Mo2a1P/Hfkw5tw9FLzK62bj34Jl6VNbQfRV6eJcA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@tanstack/table-core": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", @@ -3769,6 +3787,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.22", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.22.tgz", + "integrity": "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tiptap/core": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 81de41a1..76773512 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "@react-three/fiber": "^9.4.0", "@tanstack/react-query": "^5.86.0", "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.22", "@tiptap/core": "^2.27.1", "@tiptap/extension-placeholder": "^2.27.1", "@tiptap/pm": "^2.27.1", From 1a319d178585abfc570a7768e073c9d8842a2424 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 16 Mar 2026 14:51:34 +0900 Subject: [PATCH 18/18] feat: enhance V2TimelineSchedulerConfigPanel with filter and view mode options - Added new filter and linking settings section to the V2TimelineSchedulerConfigPanel, allowing users to manage static filters and linked filters more effectively. - Introduced view mode options to switch between different display modes in the timeline scheduler. - Updated the configuration types and added new toolbar action settings to support custom actions in the timeline toolbar. - Enhanced the overall user experience by providing more flexible filtering and display options. These updates aim to improve the functionality and usability of the timeline scheduler within the ERP system, enabling better data management and visualization. Made-with: Cursor --- .../V2TimelineSchedulerConfigPanel.tsx | 651 +++++++++++++++++- .../TimelineSchedulerComponent.tsx | 400 +++++------ .../v2-timeline-scheduler/config.ts | 35 +- .../components/v2-timeline-scheduler/types.ts | 55 ++ 4 files changed, 918 insertions(+), 223 deletions(-) diff --git a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx index 11815db8..44065912 100644 --- a/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TimelineSchedulerConfigPanel.tsx @@ -15,11 +15,11 @@ import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; -import { Settings, ChevronDown, Check, ChevronsUpDown, Database, Users, Layers } from "lucide-react"; +import { Settings, ChevronDown, Check, ChevronsUpDown, Database, Users, Layers, Filter, Link, Zap, Trash2, Plus, GripVertical } from "lucide-react"; import { cn } from "@/lib/utils"; import { tableTypeApi } from "@/lib/api/screen"; -import type { TimelineSchedulerConfig, ScheduleType, SourceDataConfig, ResourceFieldMapping, FieldMapping, ZoomLevel } from "@/lib/registry/components/v2-timeline-scheduler/types"; -import { zoomLevelOptions, scheduleTypeOptions } from "@/lib/registry/components/v2-timeline-scheduler/config"; +import type { TimelineSchedulerConfig, ScheduleType, SourceDataConfig, ResourceFieldMapping, FieldMapping, ZoomLevel, ToolbarAction } from "@/lib/registry/components/v2-timeline-scheduler/types"; +import { zoomLevelOptions, scheduleTypeOptions, viewModeOptions, dataSourceOptions, toolbarIconOptions } from "@/lib/registry/components/v2-timeline-scheduler/config"; interface V2TimelineSchedulerConfigPanelProps { config: TimelineSchedulerConfig; @@ -49,10 +49,16 @@ export const V2TimelineSchedulerConfigPanel: React.FC(null); useEffect(() => { const loadTables = async () => { @@ -225,6 +231,31 @@ export const V2TimelineSchedulerConfigPanel: React.FC
+ {/* 뷰 모드 */} +
+
+

표시 모드

+

+ {viewModeOptions.find((o) => o.value === (config.viewMode || "resource"))?.description} +

+
+ +
+ {/* 커스텀 테이블 사용 여부 */}
@@ -470,6 +501,210 @@ export const V2TimelineSchedulerConfigPanel: React.FC + {/* ─── 필터 & 연동 설정 ─── */} + + + + + +
+ {/* 정적 필터 */} +
+

정적 필터 (staticFilters)

+

데이터 조회 시 항상 적용되는 고정 필터 조건

+ + {Object.entries(config.staticFilters || {}).map(([key, value]) => ( +
+ + = + + +
+ ))} + +
+ setNewFilterKey(e.target.value)} + placeholder="필드명 (예: product_type)" + className="h-7 flex-1 text-xs" + /> + = + setNewFilterValue(e.target.value)} + placeholder="값 (예: 완제품)" + className="h-7 flex-1 text-xs" + /> + +
+
+ + {/* 구분선 */} +
+ + {/* 연결 필터 */} +
+
+
+

+ + 연결 필터 (linkedFilter) +

+

다른 컴포넌트 선택에 따라 데이터를 필터링

+
+ { + if (v) { + updateConfig({ + linkedFilter: { + sourceField: "", + targetField: "", + showEmptyWhenNoSelection: true, + emptyMessage: "좌측 목록에서 항목을 선택하세요", + }, + }); + } else { + updateConfig({ linkedFilter: undefined }); + } + }} + /> +
+ + {config.linkedFilter && ( +
+
+
+ 소스 테이블명 +

선택 이벤트의 tableName 매칭

+
+ + + + + + value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0}> + + + 없음 + + {tables.map((table) => ( + { + updateConfig({ + linkedFilter: { ...config.linkedFilter!, sourceTableName: table.tableName }, + }); + setLinkedFilterTableOpen(false); + }} + className="text-xs" + > + + {table.displayName} + + ))} + + + + + +
+ +
+ 소스 필드 (sourceField) * + updateConfig({ linkedFilter: { ...config.linkedFilter!, sourceField: e.target.value } })} + placeholder="예: part_code" + className="h-7 w-[140px] text-xs" + /> +
+ +
+ 타겟 필드 (targetField) * + updateConfig({ linkedFilter: { ...config.linkedFilter!, targetField: e.target.value } })} + placeholder="예: item_code" + className="h-7 w-[140px] text-xs" + /> +
+ +
+ 빈 상태 메시지 + updateConfig({ linkedFilter: { ...config.linkedFilter!, emptyMessage: e.target.value } })} + placeholder="선택 안내 문구" + className="h-7 w-[180px] text-xs" + /> +
+ +
+ 선택 없을 때 빈 화면 + updateConfig({ linkedFilter: { ...config.linkedFilter!, showEmptyWhenNoSelection: v } })} + /> +
+
+ )} +
+
+ + + {/* ─── 2단계: 소스 데이터 설정 ─── */} @@ -1038,6 +1273,17 @@ export const V2TimelineSchedulerConfigPanel: React.FC updateConfig({ showAddButton: v })} />
+ +
+
+

범례 표시

+

상태별 색상 범례를 보여줘요

+
+ updateConfig({ showLegend: v })} + /> +
@@ -1114,6 +1360,405 @@ export const V2TimelineSchedulerConfigPanel: React.FC + {/* ─── 6단계: 툴바 액션 설정 ─── */} + + + + + +
+

+ 툴바에 커스텀 버튼을 추가하여 API 호출 (미리보기 → 확인 → 적용) 워크플로우를 구성해요 +

+ + {/* 기존 액션 목록 */} + {(config.toolbarActions || []).map((action, index) => ( + setExpandedActionId(open ? action.id : null)} + > +
+ + + +
+ + + +
+ {/* 기본 설정 */} +
+
+ 버튼명 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], label: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + className="h-7 text-xs" + /> +
+
+ 아이콘 + +
+
+ +
+ 버튼 색상 (Tailwind 클래스) + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], color: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="예: bg-emerald-600 hover:bg-emerald-700" + className="h-7 text-xs" + /> +
+ + {/* API 설정 */} +
+

API 설정

+
+
+ 미리보기 API * + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], previewApi: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="/production/generate-schedule/preview" + className="h-7 text-xs" + /> +
+
+ 적용 API * + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], applyApi: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="/production/generate-schedule" + className="h-7 text-xs" + /> +
+
+
+ + {/* 다이얼로그 설정 */} +
+

다이얼로그

+
+
+ 제목 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], dialogTitle: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="자동 생성" + className="h-7 text-xs" + /> +
+
+ 설명 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], dialogDescription: e.target.value }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="미리보기 후 확인하여 적용합니다" + className="h-7 text-xs" + /> +
+
+
+ + {/* 데이터 소스 설정 */} +
+

데이터 소스

+
+
+ 데이터 소스 유형 * + +
+ + {action.dataSource === "linkedSelection" && ( +
+
+
+ 그룹 필드 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, groupByField: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="linkedFilter.sourceField 사용" + className="h-7 text-xs" + /> +
+
+ 수량 필드 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, quantityField: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="balance_qty" + className="h-7 text-xs" + /> +
+
+
+
+ 기준일 필드 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, dueDateField: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="due_date" + className="h-7 text-xs" + /> +
+
+ 표시명 필드 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, nameField: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="part_name" + className="h-7 text-xs" + /> +
+
+
+ )} + + {action.dataSource === "currentSchedules" && ( +
+
+
+ 필터 필드 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, scheduleFilterField: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="product_type" + className="h-7 text-xs" + /> +
+
+ 필터 값 + { + const updated = [...(config.toolbarActions || [])]; + updated[index] = { ...updated[index], payloadConfig: { ...updated[index].payloadConfig, scheduleFilterValue: e.target.value || undefined } }; + updateConfig({ toolbarActions: updated }); + }} + placeholder="완제품" + className="h-7 text-xs" + /> +
+
+
+ )} +
+
+ + {/* 표시 조건 */} +
+

표시 조건 (showWhen)

+

staticFilters 값과 비교하여 일치할 때만 버튼 표시

+ {Object.entries(action.showWhen || {}).map(([key, value]) => ( +
+ + = + + +
+ ))} +
+ + = + + +
+
+
+
+
+
+ ))} + + {/* 액션 추가 버튼 */} + +
+ +
); }; diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx index 3a69da65..075e8eca 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx +++ b/frontend/lib/registry/components/v2-timeline-scheduler/TimelineSchedulerComponent.tsx @@ -12,7 +12,15 @@ import { Package, Zap, RefreshCw, + Download, + Upload, + Play, + FileText, + Send, + Sparkles, + Wand2, } from "lucide-react"; +import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { useVirtualizer } from "@tanstack/react-virtual"; @@ -20,6 +28,7 @@ import { TimelineSchedulerComponentProps, ScheduleItem, ZoomLevel, + ToolbarAction, } from "./types"; import { useTimelineData } from "./hooks/useTimelineData"; import { TimelineHeader, ResourceRow, TimelineLegend, ItemTimelineCard, groupSchedulesByItem, SchedulePreviewDialog } from "./components"; @@ -53,24 +62,24 @@ export function TimelineSchedulerComponent({ }: TimelineSchedulerComponentProps) { const containerRef = useRef(null); - // ────────── 자동 스케줄 생성 상태 ────────── - const [showPreviewDialog, setShowPreviewDialog] = useState(false); - const [previewLoading, setPreviewLoading] = useState(false); - const [previewApplying, setPreviewApplying] = useState(false); - const [previewSummary, setPreviewSummary] = useState(null); - const [previewItems, setPreviewItems] = useState([]); - const [previewDeleted, setPreviewDeleted] = useState([]); - const [previewKept, setPreviewKept] = useState([]); + // ────────── 툴바 액션 다이얼로그 상태 (통합) ────────── + const [actionDialog, setActionDialog] = useState<{ + actionId: string; + action: ToolbarAction; + isLoading: boolean; + isApplying: boolean; + summary: any; + previews: any[]; + deletedSchedules: any[]; + keptSchedules: any[]; + preparedPayload: any; + } | null>(null); const linkedFilterValuesRef = useRef([]); - // ────────── 반제품 계획 생성 상태 ────────── - const [showSemiPreviewDialog, setShowSemiPreviewDialog] = useState(false); - const [semiPreviewLoading, setSemiPreviewLoading] = useState(false); - const [semiPreviewApplying, setSemiPreviewApplying] = useState(false); - const [semiPreviewSummary, setSemiPreviewSummary] = useState(null); - const [semiPreviewItems, setSemiPreviewItems] = useState([]); - const [semiPreviewDeleted, setSemiPreviewDeleted] = useState([]); - const [semiPreviewKept, setSemiPreviewKept] = useState([]); + // ────────── 아이콘 맵 ────────── + const TOOLBAR_ICONS: Record> = useMemo(() => ({ + Zap, Package, Plus, Download, Upload, RefreshCw, Play, FileText, Send, Sparkles, Wand2, + }), []); // ────────── linkedFilter 상태 ────────── const linkedFilter = config.linkedFilter; @@ -339,197 +348,153 @@ export function TimelineSchedulerComponent({ } }, [onAddSchedule, effectiveResources]); - // ────────── 자동 스케줄 생성: 미리보기 요청 ────────── - const handleAutoSchedulePreview = useCallback(async () => { - const selectedRows = linkedFilterValuesRef.current; - if (!selectedRows || selectedRows.length === 0) { - toast.warning("좌측에서 품목을 선택해주세요"); - return; + // ────────── 유효 툴바 액션 (config 기반 또는 하위호환 자동생성) ────────── + const effectiveToolbarActions: ToolbarAction[] = useMemo(() => { + if (config.toolbarActions && config.toolbarActions.length > 0) { + return config.toolbarActions; } + return []; + }, [config.toolbarActions]); - const sourceField = config.linkedFilter?.sourceField || "part_code"; - const grouped = new Map(); - selectedRows.forEach((row: any) => { - const key = row[sourceField] || ""; - if (!key) return; - if (!grouped.has(key)) grouped.set(key, []); - grouped.get(key)!.push(row); - }); + // ────────── 범용 액션: 미리보기 요청 ────────── + const handleActionPreview = useCallback(async (action: ToolbarAction) => { + let payload: any; - const items = Array.from(grouped.entries()).map(([itemCode, rows]) => { - const totalBalanceQty = rows.reduce((sum: number, r: any) => sum + (Number(r.balance_qty) || 0), 0); - const earliestDueDate = rows - .map((r: any) => r.due_date) - .filter(Boolean) - .sort()[0] || new Date().toISOString().split("T")[0]; - const first = rows[0]; + if (action.dataSource === "linkedSelection") { + const selectedRows = linkedFilterValuesRef.current; + if (!selectedRows || selectedRows.length === 0) { + toast.warning("좌측에서 항목을 선택해주세요"); + return; + } - return { - item_code: itemCode, - item_name: first.part_name || first.item_name || itemCode, - required_qty: totalBalanceQty, - earliest_due_date: typeof earliestDueDate === "string" ? earliestDueDate.split("T")[0] : earliestDueDate, - hourly_capacity: Number(first.hourly_capacity) || undefined, - daily_capacity: Number(first.daily_capacity) || undefined, - }; - }).filter((item) => item.required_qty > 0); + const groupField = action.payloadConfig?.groupByField || config.linkedFilter?.sourceField || "part_code"; + const qtyField = action.payloadConfig?.quantityField || config.sourceConfig?.quantityField || "balance_qty"; + const dateField = action.payloadConfig?.dueDateField || config.sourceConfig?.dueDateField || "due_date"; + const nameField = action.payloadConfig?.nameField || config.sourceConfig?.groupNameField || "part_name"; - if (items.length === 0) { - toast.warning("선택된 품목의 잔량이 없습니다"); - return; - } - - setShowPreviewDialog(true); - setPreviewLoading(true); - - try { - const response = await apiClient.post("/production/generate-schedule/preview", { - items, - options: { - product_type: config.staticFilters?.product_type || "완제품", - safety_lead_time: 1, - recalculate_unstarted: true, - }, + const grouped = new Map(); + selectedRows.forEach((row: any) => { + const key = row[groupField] || ""; + if (!key) return; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(row); }); + const items = Array.from(grouped.entries()).map(([code, rows]) => { + const totalQty = rows.reduce((sum: number, r: any) => sum + (Number(r[qtyField]) || 0), 0); + const dates = rows.map((r: any) => r[dateField]).filter(Boolean).sort(); + const earliestDate = dates[0] || new Date().toISOString().split("T")[0]; + const first = rows[0]; + return { + item_code: code, + item_name: first[nameField] || first.item_name || code, + required_qty: totalQty, + earliest_due_date: typeof earliestDate === "string" ? earliestDate.split("T")[0] : earliestDate, + hourly_capacity: Number(first.hourly_capacity) || undefined, + daily_capacity: Number(first.daily_capacity) || undefined, + }; + }).filter((item) => item.required_qty > 0); + + if (items.length === 0) { + toast.warning("선택된 항목의 잔량이 없습니다"); + return; + } + + payload = { + items, + options: { + ...(config.staticFilters || {}), + ...(action.payloadConfig?.extraOptions || {}), + }, + }; + } else if (action.dataSource === "currentSchedules") { + let targetSchedules = schedules; + const filterField = action.payloadConfig?.scheduleFilterField; + const filterValue = action.payloadConfig?.scheduleFilterValue; + + if (filterField && filterValue) { + targetSchedules = schedules.filter((s) => { + const val = (s.data as any)?.[filterField] || ""; + return val === filterValue; + }); + } + + if (targetSchedules.length === 0) { + toast.warning("대상 스케줄이 없습니다"); + return; + } + + const planIds = targetSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id)); + if (planIds.length === 0) { + toast.warning("유효한 스케줄 ID가 없습니다"); + return; + } + + payload = { + plan_ids: planIds, + options: action.payloadConfig?.extraOptions || {}, + }; + } + + setActionDialog({ + actionId: action.id, + action, + isLoading: true, + isApplying: false, + summary: null, + previews: [], + deletedSchedules: [], + keptSchedules: [], + preparedPayload: payload, + }); + + try { + const response = await apiClient.post(action.previewApi, payload); if (response.data?.success) { - setPreviewSummary(response.data.data.summary); - setPreviewItems(response.data.data.previews); - setPreviewDeleted(response.data.data.deletedSchedules || []); - setPreviewKept(response.data.data.keptSchedules || []); + setActionDialog((prev) => prev ? { + ...prev, + isLoading: false, + summary: response.data.data.summary, + previews: response.data.data.previews || [], + deletedSchedules: response.data.data.deletedSchedules || [], + keptSchedules: response.data.data.keptSchedules || [], + } : null); } else { - toast.error("미리보기 생성 실패"); - setShowPreviewDialog(false); + toast.error("미리보기 생성 실패", { description: response.data?.message }); + setActionDialog(null); } } catch (err: any) { toast.error("미리보기 요청 실패", { description: err.message }); - setShowPreviewDialog(false); - } finally { - setPreviewLoading(false); + setActionDialog(null); } - }, [config.linkedFilter, config.staticFilters]); + }, [config.linkedFilter, config.staticFilters, config.sourceConfig, schedules]); - // ────────── 자동 스케줄 생성: 확인 및 적용 ────────── - const handleAutoScheduleApply = useCallback(async () => { - if (!previewItems || previewItems.length === 0) return; + // ────────── 범용 액션: 확인 및 적용 ────────── + const handleActionApply = useCallback(async () => { + if (!actionDialog) return; + const { action, preparedPayload } = actionDialog; - setPreviewApplying(true); - - const items = previewItems.map((p: any) => ({ - item_code: p.item_code, - item_name: p.item_name, - required_qty: p.required_qty, - earliest_due_date: p.due_date, - hourly_capacity: p.hourly_capacity, - daily_capacity: p.daily_capacity, - })); + setActionDialog((prev) => prev ? { ...prev, isApplying: true } : null); try { - const response = await apiClient.post("/production/generate-schedule", { - items, - options: { - product_type: config.staticFilters?.product_type || "완제품", - safety_lead_time: 1, - recalculate_unstarted: true, - }, - }); - - if (response.data?.success) { - const summary = response.data.data.summary; - toast.success("생산계획 업데이트 완료", { - description: `신규: ${summary.new_count}건, 유지: ${summary.kept_count}건, 삭제: ${summary.deleted_count}건`, - }); - setShowPreviewDialog(false); - refreshTimeline(); - } else { - toast.error("생산계획 생성 실패"); - } - } catch (err: any) { - toast.error("생산계획 생성 실패", { description: err.message }); - } finally { - setPreviewApplying(false); - } - }, [previewItems, config.staticFilters, refreshTimeline]); - - // ────────── 반제품 계획 생성: 미리보기 요청 ────────── - const handleSemiSchedulePreview = useCallback(async () => { - // 현재 타임라인에 표시된 완제품 스케줄의 plan ID 수집 - const finishedSchedules = schedules.filter((s) => { - const productType = (s.data as any)?.product_type || ""; - return productType === "완제품"; - }); - - if (finishedSchedules.length === 0) { - toast.warning("완제품 스케줄이 없습니다. 먼저 완제품 계획을 생성해주세요."); - return; - } - - const planIds = finishedSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id)); - if (planIds.length === 0) { - toast.warning("유효한 완제품 계획 ID가 없습니다"); - return; - } - - setShowSemiPreviewDialog(true); - setSemiPreviewLoading(true); - - try { - const response = await apiClient.post("/production/generate-semi-schedule/preview", { - plan_ids: planIds, - options: { considerStock: true }, - }); - - if (response.data?.success) { - setSemiPreviewSummary(response.data.data.summary); - setSemiPreviewItems(response.data.data.previews || []); - setSemiPreviewDeleted(response.data.data.deletedSchedules || []); - setSemiPreviewKept(response.data.data.keptSchedules || []); - } else { - toast.error("반제품 미리보기 실패", { description: response.data?.message }); - setShowSemiPreviewDialog(false); - } - } catch (err: any) { - toast.error("반제품 미리보기 요청 실패", { description: err.message }); - setShowSemiPreviewDialog(false); - } finally { - setSemiPreviewLoading(false); - } - }, [schedules]); - - // ────────── 반제품 계획 생성: 확인 및 적용 ────────── - const handleSemiScheduleApply = useCallback(async () => { - const finishedSchedules = schedules.filter((s) => { - const productType = (s.data as any)?.product_type || ""; - return productType === "완제품"; - }); - const planIds = finishedSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id)); - - if (planIds.length === 0) return; - - setSemiPreviewApplying(true); - - try { - const response = await apiClient.post("/production/generate-semi-schedule", { - plan_ids: planIds, - options: { considerStock: true }, - }); - + const response = await apiClient.post(action.applyApi, preparedPayload); if (response.data?.success) { const data = response.data.data; - toast.success("반제품 계획 생성 완료", { - description: `${data.count}건의 반제품 계획이 생성되었습니다`, + const summary = data.summary || data; + toast.success(action.dialogTitle || "완료", { + description: `신규: ${summary.new_count || summary.count || 0}건${summary.kept_count ? `, 유지: ${summary.kept_count}건` : ""}${summary.deleted_count ? `, 삭제: ${summary.deleted_count}건` : ""}`, }); - setShowSemiPreviewDialog(false); + setActionDialog(null); refreshTimeline(); } else { - toast.error("반제품 계획 생성 실패"); + toast.error("실행 실패", { description: response.data?.message }); } } catch (err: any) { - toast.error("반제품 계획 생성 실패", { description: err.message }); + toast.error("실행 실패", { description: err.message }); } finally { - setSemiPreviewApplying(false); + setActionDialog((prev) => prev ? { ...prev, isApplying: false } : null); } - }, [schedules, refreshTimeline]); + }, [actionDialog, refreshTimeline]); // ────────── 하단 영역 높이 계산 (툴바 + 범례) ────────── const showToolbar = config.showToolbar !== false; @@ -713,18 +678,26 @@ export function TimelineSchedulerComponent({ 새로고침 - {config.staticFilters?.product_type === "완제품" && ( - <> - - - - )} + ); + })}
)} @@ -796,33 +769,22 @@ export function TimelineSchedulerComponent({
)} - {/* 완제품 스케줄 생성 미리보기 다이얼로그 */} - - - {/* 반제품 계획 생성 미리보기 다이얼로그 */} - + {/* 범용 액션 미리보기 다이얼로그 */} + {actionDialog && ( + { if (!open) setActionDialog(null); }} + isLoading={actionDialog.isLoading} + summary={actionDialog.summary} + previews={actionDialog.previews} + deletedSchedules={actionDialog.deletedSchedules} + keptSchedules={actionDialog.keptSchedules} + onConfirm={handleActionApply} + isApplying={actionDialog.isApplying} + title={actionDialog.action.dialogTitle} + description={actionDialog.action.dialogDescription} + /> + )}
); } diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/config.ts b/frontend/lib/registry/components/v2-timeline-scheduler/config.ts index 17c31991..57409191 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/config.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/config.ts @@ -1,6 +1,6 @@ "use client"; -import { TimelineSchedulerConfig, ZoomLevel, ScheduleType } from "./types"; +import { TimelineSchedulerConfig, ZoomLevel, ScheduleType, ToolbarAction } from "./types"; /** * 기본 타임라인 스케줄러 설정 @@ -94,6 +94,39 @@ export const scheduleTypeOptions: { value: ScheduleType; label: string }[] = [ { value: "WORK_ASSIGN", label: "작업배정" }, ]; +/** + * 뷰 모드 옵션 + */ +export const viewModeOptions: { value: string; label: string; description: string }[] = [ + { value: "resource", label: "리소스 기반", description: "설비/작업자 행 기반 간트차트" }, + { value: "itemGrouped", label: "품목별 그룹", description: "품목별 카드형 타임라인" }, +]; + +/** + * 데이터 소스 옵션 + */ +export const dataSourceOptions: { value: string; label: string; description: string }[] = [ + { value: "linkedSelection", label: "연결 필터 선택값", description: "좌측 테이블에서 선택된 행 데이터 사용" }, + { value: "currentSchedules", label: "현재 스케줄", description: "타임라인에 표시 중인 스케줄 ID 사용" }, +]; + +/** + * 아이콘 옵션 + */ +export const toolbarIconOptions: { value: string; label: string }[] = [ + { value: "Zap", label: "Zap (번개)" }, + { value: "Package", label: "Package (박스)" }, + { value: "Plus", label: "Plus (추가)" }, + { value: "Download", label: "Download (다운로드)" }, + { value: "Upload", label: "Upload (업로드)" }, + { value: "RefreshCw", label: "RefreshCw (새로고침)" }, + { value: "Play", label: "Play (재생)" }, + { value: "FileText", label: "FileText (문서)" }, + { value: "Send", label: "Send (전송)" }, + { value: "Sparkles", label: "Sparkles (반짝)" }, + { value: "Wand2", label: "Wand2 (마법봉)" }, +]; + /** * 줌 레벨별 표시 일수 */ diff --git a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts index aa5c4edd..5c0ef953 100644 --- a/frontend/lib/registry/components/v2-timeline-scheduler/types.ts +++ b/frontend/lib/registry/components/v2-timeline-scheduler/types.ts @@ -128,6 +128,58 @@ export interface SourceDataConfig { groupNameField?: string; } +/** + * 툴바 액션 설정 (커스텀 버튼) + * 타임라인 툴바에 표시되는 커스텀 액션 버튼을 정의 + * preview -> confirm -> apply 워크플로우 지원 + */ +export interface ToolbarAction { + /** 고유 ID */ + id: string; + /** 버튼 텍스트 */ + label: string; + /** lucide-react 아이콘명 */ + icon?: "Zap" | "Package" | "Plus" | "Download" | "Upload" | "RefreshCw" | "Play" | "FileText" | "Send" | "Sparkles" | "Wand2"; + /** 버튼 색상 클래스 (예: "bg-emerald-600 hover:bg-emerald-700") */ + color?: string; + /** 미리보기 API 엔드포인트 (예: "/production/generate-schedule/preview") */ + previewApi: string; + /** 적용 API 엔드포인트 (예: "/production/generate-schedule") */ + applyApi: string; + /** 다이얼로그 제목 */ + dialogTitle?: string; + /** 다이얼로그 설명 */ + dialogDescription?: string; + /** + * 데이터 소스 유형 + * - linkedSelection: 연결 필터(좌측 테이블)에서 선택된 행 사용 + * - currentSchedules: 현재 타임라인의 스케줄 ID 사용 + */ + dataSource: "linkedSelection" | "currentSchedules"; + /** 페이로드 구성 설정 */ + payloadConfig?: { + /** linkedSelection: 선택된 행을 그룹화할 필드 (기본: linkedFilter.sourceField) */ + groupByField?: string; + /** linkedSelection: 수량 합계 필드 (예: "balance_qty") */ + quantityField?: string; + /** linkedSelection: 기준일 필드 (예: "due_date") */ + dueDateField?: string; + /** linkedSelection: 표시명 필드 (예: "part_name") */ + nameField?: string; + /** currentSchedules: 스케줄 필터 조건 필드명 (예: "product_type") */ + scheduleFilterField?: string; + /** currentSchedules: 스케줄 필터 값 (예: "완제품") */ + scheduleFilterValue?: string; + /** API 호출 시 추가 옵션 (예: { "safety_lead_time": 1 }) */ + extraOptions?: Record; + }; + /** + * 표시 조건: staticFilters와 비교하여 모든 조건이 일치할 때만 버튼 표시 + * 예: { "product_type": "완제품" } → staticFilters.product_type === "완제품"일 때만 표시 + */ + showWhen?: Record; +} + /** * 타임라인 스케줄러 설정 */ @@ -254,6 +306,9 @@ export interface TimelineSchedulerConfig extends ComponentConfig { /** 빈 상태 메시지 */ emptyMessage?: string; }; + + /** 툴바 커스텀 액션 버튼 설정 */ + toolbarActions?: ToolbarAction[]; } /**