feat: 수정 모드 UPSERT 기능 구현

- SelectedItemsDetailInput 컴포넌트 수정 모드 지원
- 그룹화된 데이터 UPSERT API 추가 (/api/data/upsert-grouped)
- 부모 키 기준으로 기존 레코드 조회 후 INSERT/UPDATE/DELETE
- 각 레코드의 모든 필드 조합을 고유 키로 사용
- created_date 보존 (UPDATE 시)
- 수정 모드에서 groupByColumns 기준으로 관련 레코드 조회
- 날짜 타입 ISO 형식 자동 감지 및 포맷팅 (YYYY.MM.DD)

주요 변경사항:
- backend: dataService.upsertGroupedRecords() 메서드 구현
- backend: dataRoutes POST /api/data/upsert-grouped 엔드포인트 추가
- frontend: ScreenModal에서 groupByColumns 파라미터 전달
- frontend: SelectedItemsDetailInput 수정 모드 로직 추가
- frontend: 날짜 필드 타임존 제거 및 포맷팅 개선
This commit is contained in:
kjs
2025-11-20 10:23:54 +09:00
parent d4895c363c
commit 34cd7ba9e3
13 changed files with 2704 additions and 345 deletions

View File

@@ -6,9 +6,28 @@
export interface ColumnFilter {
id: string;
columnName: string;
operator: "equals" | "not_equals" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | "is_null" | "is_not_null";
operator:
| "equals"
| "not_equals"
| "in"
| "not_in"
| "contains"
| "starts_with"
| "ends_with"
| "is_null"
| "is_not_null"
| "greater_than"
| "less_than"
| "greater_than_or_equal"
| "less_than_or_equal"
| "between"
| "date_range_contains";
value: string | string[];
valueType: "static" | "category" | "code";
valueType: "static" | "category" | "code" | "dynamic";
rangeConfig?: {
startColumn: string;
endColumn: string;
};
}
export interface DataFilterConfig {
@@ -123,6 +142,71 @@ export function buildDataFilterWhereClause(
conditions.push(`${columnRef} IS NOT NULL`);
break;
case "greater_than":
conditions.push(`${columnRef} > $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "less_than":
conditions.push(`${columnRef} < $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "greater_than_or_equal":
conditions.push(`${columnRef} >= $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "less_than_or_equal":
conditions.push(`${columnRef} <= $${paramIndex}`);
params.push(value);
paramIndex++;
break;
case "between":
if (Array.isArray(value) && value.length === 2) {
conditions.push(`${columnRef} BETWEEN $${paramIndex} AND $${paramIndex + 1}`);
params.push(value[0], value[1]);
paramIndex += 2;
}
break;
case "date_range_contains":
// 날짜 범위 포함: start_date <= value <= end_date
// filter.rangeConfig = { startColumn: "start_date", endColumn: "end_date" }
// NULL 처리:
// - start_date만 있고 end_date가 NULL이면: start_date <= value (이후 계속)
// - end_date만 있고 start_date가 NULL이면: value <= end_date (이전 계속)
// - 둘 다 있으면: start_date <= value <= end_date
if (filter.rangeConfig && filter.rangeConfig.startColumn && filter.rangeConfig.endColumn) {
const startCol = getColumnRef(filter.rangeConfig.startColumn);
const endCol = getColumnRef(filter.rangeConfig.endColumn);
// value가 "TODAY"면 현재 날짜로 변환
const actualValue = filter.valueType === "dynamic" && value === "TODAY"
? "CURRENT_DATE"
: `$${paramIndex}`;
if (actualValue === "CURRENT_DATE") {
// CURRENT_DATE는 파라미터가 아니므로 직접 SQL에 포함
// NULL 처리: (start_date IS NULL OR start_date <= CURRENT_DATE) AND (end_date IS NULL OR end_date >= CURRENT_DATE)
conditions.push(
`((${startCol} IS NULL OR ${startCol} <= CURRENT_DATE) AND (${endCol} IS NULL OR ${endCol} >= CURRENT_DATE))`
);
} else {
// NULL 처리: (start_date IS NULL OR start_date <= $param) AND (end_date IS NULL OR end_date >= $param)
conditions.push(
`((${startCol} IS NULL OR ${startCol} <= $${paramIndex}) AND (${endCol} IS NULL OR ${endCol} >= $${paramIndex}))`
);
params.push(value);
paramIndex++;
}
}
break;
default:
// 알 수 없는 연산자는 무시
break;