feat: 개선된 데이터 필터링 로직 및 동적 검색 필터 옵션 로드 기능 추가
This commit is contained in:
@@ -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 전체 명세 포함 필요
|
||||
**선택 (품질 향상)**:
|
||||
3. ref 없는 태스크에 "COMPANY_16 현재 코드의 API 패턴이 올바른지 확인하고, 누락된 기능만 추가"로 방향 변경 고려
|
||||
|
||||
질문이 있으시면 말씀해 주세요.
|
||||
@@ -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) {
|
||||
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(value[0], value[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
|
||||
|
||||
@@ -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<string>();
|
||||
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({
|
||||
<ChevronsUpDown className="ml-1 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
{options.length > 5 && (
|
||||
<PopoverContent className="p-0" style={{ minWidth: "var(--radix-popover-trigger-width)", width: "auto", maxWidth: "400px" }} align="start">
|
||||
{options.length > 0 && (
|
||||
<div className="border-b p-1">
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2" />
|
||||
|
||||
Reference in New Issue
Block a user