diff --git a/_pipeline/reviews/plan-review.md b/_pipeline/reviews/plan-review.md index 52e7b86f..b2658c75 100644 --- a/_pipeline/reviews/plan-review.md +++ b/_pipeline/reviews/plan-review.md @@ -1,91 +1,86 @@ -# Plan Review Report: API 로직 전체 복구 + 테이블 설정 인라인 구현 +# Plan Review Report: `screen-api-fix-v2` ### 1. 플랜 요약 -COMPANY_7 원본 기준으로 COMPANY_16의 전 화면(43개 파일, 20개 태스크) 데이터 흐름을 동기화하고, 해당 화면에 테이블 설정 기능을 인라인 구현. +COMPANY_16의 전체 화면(43개 파일)에서 깨진 데이터 흐름을 COMPANY_7 원본 기준으로 복구하고, 테이블 설정 기능을 인라인으로 구현하는 작업. 22개 태스크, 전부 병렬. --- ### 2. 문제점 지적 -#### 🔴 수정 필요: ref_files 12개 미존재 +#### 🔴 수정 필요: ref_files 10개 존재하지 않음 -다음 COMPANY_7 원본 파일이 존재하지 않아 에이전트가 참조할 수 없습니다: +에이전트가 "COMPANY_7 원본을 읽고 동기화하라"는 지시를 받지만, 참고할 원본 파일이 없습니다: -| 태스크 | 누락된 ref_file | -|--------|----------------| -| task-10 (BOM) | `COMPANY_7/production/bom/page.tsx` | -| task-11a (발주) | `COMPANY_7/purchase/order/page.tsx` | -| task-11b (구매품목) | `COMPANY_7/purchase/purchase-item/page.tsx` | -| task-11b (공급업체) | `COMPANY_7/purchase/supplier/page.tsx` | -| task-13b (재고) | `COMPANY_7/logistics/inventory/page.tsx` | -| task-13b (창고) | `COMPANY_7/logistics/warehouse/page.tsx` | -| task-13b (물류정보) | `COMPANY_7/logistics/info/page.tsx` | -| task-14 (금형) | `COMPANY_7/mold/info/page.tsx` | -| task-15a (회사) | `COMPANY_7/master-data/company/page.tsx` | -| task-15b (검사) | `COMPANY_7/quality/inspection/page.tsx` | -| task-15b (품목검사) | `COMPANY_7/quality/item-inspection/page.tsx` | -| task-15b (PLC) | `COMPANY_7/equipment/plc-settings/page.tsx` | +| task | 누락된 ref_file | 영향 | +|------|----------------|------| +| **task-10** (BOM) | `COMPANY_7/production/bom/page.tsx` | COMPANY_7에 BOM 디렉토리 자체 없음 | +| **task-11a** (발주) | `COMPANY_7/purchase/order/page.tsx` | COMPANY_7/purchase 디렉토리 자체 없음 | +| **task-11b** (구매품목+공급업체) | `COMPANY_7/purchase/purchase-item/page.tsx`, `COMPANY_7/purchase/supplier/page.tsx` | 위와 동일 | +| **task-13b** (재고+창고+물류정보) | `COMPANY_7/logistics/inventory/page.tsx`, `COMPANY_7/logistics/warehouse/page.tsx`, `COMPANY_7/logistics/info/page.tsx` | COMPANY_7에 해당 3개 디렉토리 없음 | +| **task-14** (금형) | `COMPANY_7/mold/info/page.tsx` | COMPANY_7에 mold 디렉토리 없음 | +| **task-15a** (회사정보) | `COMPANY_7/master-data/company/page.tsx` | 부서/품목은 있지만 회사정보는 없음 | +| **task-15b** (품질+PLC) | `COMPANY_7/quality/inspection/page.tsx`, `COMPANY_7/quality/item-inspection/page.tsx`, `COMPANY_7/equipment/plc-settings/page.tsx` | COMPANY_7에 quality, plc-settings 전부 없음 | -→ 이 12개 ref_file이 없으면 에이전트는 "COMPANY_7 기준으로 동기화"라는 지시를 수행할 수 없습니다. **플랜 실행 전 반드시 해결 필요.** +**결과**: 에이전트가 ref_files를 읽으려 하면 파일 없음 에러 → context의 "COMPANY_7 원본과 비교하여" 지시를 수행 불가 → 자체 판단으로 최소 수정만 하고 "완료" 보고할 가능성 높음. ---- +#### 🔴 수정 필요: task-17a/17b ref_files 자기참조 -#### 🟠 도주 위험 +task-17a, 17b의 ref_files가 files와 **동일한 파일**을 가리킵니다. 예: +- files: `admin/report/sales/page.tsx` +- ref_files: `admin/report/sales/page.tsx` -**task-11a (발주관리)**: context가 "수주관리와 동일 패턴"으로 5줄. ref_file도 누락. 에이전트가 최소 수정만 하고 "완료" 보고할 가능성 높음. +"원본과 일치하는지 확인하고 누락분 보강"이라고 했지만, 비교 대상이 자기 자신이라 에이전트가 "이미 일치함"으로 판단하고 아무것도 안 할 가능성이 높습니다. -**task-11b (구매품목+공급업체)**: 2개 파일인데 context가 "판매품목과 동일 패턴"/"거래처관리와 동일 패턴" 한 줄씩. ref_file도 둘 다 누락. 이중 위험. +#### 🟠 도주 위험: ref 없는 태스크 6개 -**task-17a/17b (리포트)**: "이미 이전 파이프라인에서 수정됨. 누락분 보강" → 에이전트가 "확인 결과 이미 충족" 판단 후 아무것도 안 할 가능성. ref_files가 자기 자신(files == ref_files)이므로 비교 대상이 없음. - -**task-14 (외주+설비+금형)**: 4개 파일인데 각 파일별 context가 한 줄. 금형은 ref_file도 누락. - ---- - -#### 🟡 충돌 감지 - -파일 겹침은 없습니다. 모든 태스크의 files가 고유합니다. depends도 전부 none으로 순환 없음. +| task | 유형 | 위험도 | 이유 | +|------|------|--------|------| +| **task-10** | refactor | **높음** | BOM은 복잡한 트리 구조인데 참고 원본 없음 | +| **task-11a/11b** | refactor | **높음** | "수주관리와 동일 패턴"이라고만 되어 있고 원본 없음 | +| **task-13b** | refactor | **중간** | 3개 파일 모두 ref 없음, 비교 불가 | +| **task-15b** | refactor | **높음** | 3개 파일 전부 ref 없음 | +| **task-17a/17b** | formatting | **낮음** | 이미 수정된 파일의 보강이므로 변경량이 적을 수 있음 | --- ### 3. 수정 범위 예상 -| 항목 | 수치 | -|------|------| -| 총 태스크 수 | 20개 | -| 대상 파일 수 | 43개 | -| ref_file 존재 | 60개 중 48개 (12개 누락) | -| 예상 변경 줄 수 | 태스크당 200~800줄 × 20 = **4,000~16,000줄** | +- **대상 파일**: 43개 (COMPANY_16 36개 + admin/report 7개) +- **현재 총 코드량**: 약 38,311줄 +- **예상 변경량**: 파일당 평균 100~300줄 변경 시 → 총 5,000~12,000줄 diff 예상 +- **태스크당 파일 수**: 최소 1개, 최대 4개 (모두 7개 이하 — OK) --- ### 4. 예상 구동시간 -- 20 태스크, max_concurrent: 5 → **4 웨이브** -- timeout: 30m/태스크 -- 최악: 4 × 30m = **2시간** -- 현실적: 대부분 1~2파일 태스크 → **1~1.5시간** +- 22개 태스크, `max_concurrent: 5`, 전부 `depends: none` (완전 병렬) +- 라운드 수: ceil(22/5) = **5라운드** +- 타임아웃: 30분/태스크 +- **예상**: 라운드당 20~30분 × 5라운드 = **100~150분** (1.5~2.5시간) +- 재시도 포함 최악: 3시간 --- ### 5. 검증 단계 확인 -| 검증 | 현재 상태 | 비고 | -|------|----------|------| -| L1 (tsc --noEmit) | ✅ 전 태스크 설정됨 | 양호 | -| L6 (verify/grep) | ✅ 전 태스크 설정됨 | 대부분 주요 함수명/패턴 grep | -| L3 (api_test) | ✅ 전 태스크 설정됨 | 실제 API 호출로 검증 | +| 검증 | 설정 | 상태 | +|------|------|------| +| **L1 (test)** | `npx tsc --noEmit` | 전 태스크 설정됨 ✅ | +| **L6 (verify)** | grep 기반 | 전 태스크 설정됨 ✅ | +| **L3 (api_test)** | auth + API 호출 | 전 태스크 설정됨 ✅ | -verify 품질: task-1은 3개 패턴만 체크(dataFilter, autoFilter, tableSettings)로 다소 약함. [A]~[I] 전체 로직 반영 대비 부족하나, api_test가 보완하므로 수용 가능. +**추천**: 현재 검증 구성은 양호합니다. 다만 ref_files가 없는 태스크들은 verify만으로 "제대로 동기화했는지"를 확인하기 어렵습니다. --- -### 6. 종합 판단 +### 6. 권장 조치 -**ref_files 12개 누락이 가장 큰 블로커입니다.** 이 파일들 없이 실행하면 해당 7개 태스크(task-10, 11a, 11b, 13b, 14, 15a, 15b)가 "참조할 원본 없이 추측 수정"하게 됩니다. +**필수 (실행 전 해결해야 함)**: +1. ref_files가 없는 태스크(10, 11a, 11b, 13b, 14 금형, 15a 회사, 15b) — ref 없이 작업 가능한 수준으로 context 보강하거나, 해당 태스크를 플랜에서 제거 +2. task-17a/17b — ref_files 자기참조 제거하거나, 원본 ReportConfig 스펙을 context에 직접 기재 -**추천 조치:** -1. 누락 ref_files → COMPANY_7에 해당 파일 생성하거나, 다른 경로에 있다면 경로 수정 -2. task-11a/11b → context에 task-1/task-2 수준의 상세 로직(useState 목록, API 파라미터, 저장/삭제 패턴) 추가 -3. task-17a/17b → ref_files를 자기 자신이 아닌 정답 기준 파일로 교체하거나, context에 "최소 diff N줄" 기준 추가 -4. task-14 금형 → ref_file 없이 수행 가능하도록 context에 전용 API 전체 명세 포함 필요 \ No newline at end of file +**선택 (품질 향상)**: +3. ref 없는 태스크에 "COMPANY_16 현재 코드의 API 패턴이 올바른지 확인하고, 누락된 기능만 추가"로 방향 변경 고려 + +질문이 있으시면 말씀해 주세요. \ No newline at end of file diff --git a/backend-node/src/utils/dataFilterUtil.ts b/backend-node/src/utils/dataFilterUtil.ts index 0f472331..1333d470 100644 --- a/backend-node/src/utils/dataFilterUtil.ts +++ b/backend-node/src/utils/dataFilterUtil.ts @@ -99,7 +99,14 @@ export function buildDataFilterWhereClause( break; case "in": { - const inArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : []; + let inArr: any[]; + if (Array.isArray(value)) { + inArr = value; + } else if (typeof value === "string" && value.includes("|")) { + inArr = value.split("|").filter((v: string) => v !== ""); + } else { + inArr = value != null && value !== "" ? [String(value)] : []; + } if (inArr.length > 0) { const placeholders = inArr.map((_, idx) => `$${paramIndex + idx}`).join(", "); conditions.push(`${columnRef} IN (${placeholders})`); @@ -110,7 +117,14 @@ export function buildDataFilterWhereClause( } case "not_in": { - const notInArr = Array.isArray(value) ? value : value != null && value !== "" ? [String(value)] : []; + let notInArr: any[]; + if (Array.isArray(value)) { + notInArr = value; + } else if (typeof value === "string" && value.includes("|")) { + notInArr = value.split("|").filter((v: string) => v !== ""); + } else { + notInArr = value != null && value !== "" ? [String(value)] : []; + } if (notInArr.length > 0) { const placeholders = notInArr.map((_, idx) => `$${paramIndex + idx}`).join(", "); conditions.push(`${columnRef} NOT IN (${placeholders})`); @@ -170,13 +184,32 @@ export function buildDataFilterWhereClause( paramIndex++; break; - case "between": + case "between": { + let betweenArr: any[]; if (Array.isArray(value) && value.length === 2) { - conditions.push(`${columnRef} BETWEEN $${paramIndex} AND $${paramIndex + 1}`); - params.push(value[0], value[1]); - paramIndex += 2; + betweenArr = value; + } else if (typeof value === "string" && value.includes("|")) { + betweenArr = value.split("|"); + } else { + betweenArr = []; + } + if (betweenArr.length === 2 && (betweenArr[0] || betweenArr[1])) { + if (betweenArr[0] && betweenArr[1]) { + conditions.push(`${columnRef} BETWEEN $${paramIndex} AND $${paramIndex + 1}`); + params.push(betweenArr[0], betweenArr[1]); + paramIndex += 2; + } else if (betweenArr[0]) { + conditions.push(`${columnRef} >= $${paramIndex}`); + params.push(betweenArr[0]); + paramIndex++; + } else { + conditions.push(`${columnRef} <= $${paramIndex}`); + params.push(betweenArr[1]); + paramIndex++; + } } break; + } case "date_range_contains": // 날짜 범위 포함: start_date <= value <= end_date diff --git a/frontend/components/common/DynamicSearchFilter.tsx b/frontend/components/common/DynamicSearchFilter.tsx index cfd3e709..7e3316d2 100644 --- a/frontend/components/common/DynamicSearchFilter.tsx +++ b/frontend/components/common/DynamicSearchFilter.tsx @@ -184,7 +184,7 @@ export function DynamicSearchFilter({ ); }, [externalFilterConfig]); - // select 타입 필터의 옵션 로드 + // select 타입 필터의 옵션 로드 (카테고리 → 없으면 실제 데이터 distinct) useEffect(() => { const loadOptions = async () => { const selectCols = activeFilters.filter((f) => f.filterType === "select"); @@ -204,11 +204,35 @@ export function DynamicSearchFilter({ selectCols.map(async (col) => { if (selectOptions[col.columnName]?.length) return; // 이미 로드됨 try { + // 1차: 카테고리 옵션 로드 const res = await apiClient.get(`/table-categories/${tableName}/${col.columnName}/values`); if (res.data?.success && res.data.data?.length > 0) { opts[col.columnName] = flatten(res.data.data); + return; } } catch { /* skip */ } + + // 2차: 카테고리 없으면 실제 데이터에서 distinct 값 추출 + try { + const dataRes = await apiClient.post(`/table-management/tables/${tableName}/data`, { + page: 1, size: 5000, autoFilter: true, + }); + const rawData = dataRes.data?.data; + const rows = Array.isArray(rawData) ? rawData : (rawData?.data || rawData?.rows || []); + const uniqueValues = new Set(); + for (const row of rows) { + const val = row[col.columnName]; + if (val != null && String(val).trim() !== "") { + uniqueValues.add(String(val)); + } + } + if (uniqueValues.size > 0) { + const sorted = [...uniqueValues].sort(); + opts[col.columnName] = sorted.map((v) => ({ label: v, value: v })); + } + } catch (err) { + console.warn(`[DynamicSearchFilter] ${col.columnName} distinct 로드 실패:`, err); + } }) ); if (Object.keys(opts).length > 0) { @@ -363,8 +387,8 @@ export function DynamicSearchFilter({ - - {options.length > 5 && ( + + {options.length > 0 && (