feat: 개선된 데이터 필터링 로직 및 동적 검색 필터 옵션 로드 기능 추가

This commit is contained in:
DDD1542
2026-04-03 17:01:06 +09:00
parent 1cadb71d70
commit 425e1c6cf8
3 changed files with 116 additions and 64 deletions
+50 -55
View File
@@ -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 패턴이 올바른지 확인하고, 누락된 기능만 추가"로 방향 변경 고려
질문이 있으시면 말씀해 주세요.
+39 -6
View File
@@ -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
@@ -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" />