공통코드 수정중

This commit is contained in:
kjs 2025-12-22 13:45:08 +09:00
parent ac526c8578
commit b01efd293c
9 changed files with 560 additions and 35 deletions

View File

@ -275,8 +275,22 @@ export class CommonCodeController {
const { categoryCode } = req.params;
const codeData: CreateCodeData = req.body;
const userId = req.user?.userId || "SYSTEM";
// 사용자의 회사 코드 사용 (회사별 코드 관리)
const companyCode = req.user?.companyCode || "*";
const menuObjid = req.body.menuObjid;
// 카테고리 조회 - 존재 여부 확인 및 menuObjid 가져오기
const category = await this.commonCodeService.getCategoryByCode(categoryCode);
if (!category) {
return res.status(404).json({
success: false,
message: `카테고리를 찾을 수 없습니다: ${categoryCode}`,
});
}
// 카테고리의 menuObjid 사용 (카테고리는 공통, 코드는 회사별)
const menuObjid = category.menu_objid;
logger.info(`코드 생성 - 사용자 회사로 저장: categoryCode=${categoryCode}, companyCode=${companyCode}, menuObjid=${menuObjid}`);
// 입력값 검증
if (!codeData.codeValue || !codeData.codeName) {
@ -286,13 +300,6 @@ export class CommonCodeController {
});
}
if (!menuObjid) {
return res.status(400).json({
success: false,
message: "메뉴 OBJID는 필수입니다.",
});
}
const code = await this.commonCodeService.createCode(
categoryCode,
codeData,
@ -588,4 +595,100 @@ export class CommonCodeController {
});
}
}
/**
* . ( )
* GET /api/common-codes/category-options/:tableName/:columnName
*
*
* API
*/
async getCategoryOptionsAsCode(req: AuthenticatedRequest, res: Response) {
try {
const { tableName, columnName } = req.params;
const userCompanyCode = req.user?.companyCode;
logger.info(`카테고리 → 공통코드 호환 조회: ${tableName}.${columnName}`);
const options = await this.commonCodeService.getCategoryOptionsAsCode(
tableName,
columnName,
userCompanyCode
);
return res.json({
success: true,
data: options,
message: `카테고리 옵션 조회 성공 (${tableName}.${columnName})`,
});
} catch (error) {
logger.error(`카테고리 옵션 조회 실패 (${req.params.tableName}.${req.params.columnName}):`, error);
return res.status(500).json({
success: false,
message: "카테고리 옵션 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
* GET /api/common-codes/categories/:categoryCode/hierarchy
*/
async getCodesHierarchy(req: AuthenticatedRequest, res: Response) {
try {
const { categoryCode } = req.params;
const userCompanyCode = req.user?.companyCode;
const hierarchy = await this.commonCodeService.getCodesHierarchy(
categoryCode,
userCompanyCode
);
return res.json({
success: true,
data: hierarchy,
message: `계층 코드 조회 성공 (${categoryCode})`,
});
} catch (error) {
logger.error(`계층 코드 조회 실패 (${req.params.categoryCode}):`, error);
return res.status(500).json({
success: false,
message: "계층 코드 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* ( )
* GET /api/common-codes/categories/:categoryCode/children
* Query: parentCodeValue (optional)
*/
async getChildCodes(req: AuthenticatedRequest, res: Response) {
try {
const { categoryCode } = req.params;
const { parentCodeValue } = req.query;
const userCompanyCode = req.user?.companyCode;
const children = await this.commonCodeService.getChildCodes(
categoryCode,
parentCodeValue as string | null,
userCompanyCode
);
return res.json({
success: true,
data: children,
message: `자식 코드 조회 성공 (${categoryCode}, 부모: ${parentCodeValue || '최상위'})`,
});
} catch (error) {
logger.error(`자식 코드 조회 실패 (${req.params.categoryCode}):`, error);
return res.status(500).json({
success: false,
message: "자식 코드 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
}

View File

@ -58,4 +58,20 @@ router.get("/categories/:categoryCode/options", (req, res) =>
commonCodeController.getCodeOptions(req, res)
);
// 계층 구조 코드 조회 (트리 형태)
router.get("/categories/:categoryCode/hierarchy", (req, res) =>
commonCodeController.getCodesHierarchy(req, res)
);
// 자식 코드 조회 (연쇄 선택용)
router.get("/categories/:categoryCode/children", (req, res) =>
commonCodeController.getChildCodes(req, res)
);
// 카테고리 → 공통코드 호환 API (레거시 지원)
// 기존 카테고리 타입이 공통코드로 마이그레이션된 후에도 동작
router.get("/category-options/:tableName/:columnName", (req, res) =>
commonCodeController.getCategoryOptionsAsCode(req, res)
);
export default router;

View File

@ -25,6 +25,9 @@ export interface CodeInfo {
is_active: string;
company_code: string;
menu_objid?: number | null; // 메뉴 기반 코드 관리용
// 계층 구조 지원
parent_code_value?: string | null; // 부모 코드 값
depth?: number; // 계층 깊이 (1: 최상위)
created_date?: Date | null;
created_by?: string | null;
updated_date?: Date | null;
@ -61,6 +64,9 @@ export interface CreateCodeData {
description?: string;
sortOrder?: number;
isActive?: string;
// 계층 구조 지원
parentCodeValue?: string;
depth?: number;
}
export class CommonCodeService {
@ -148,6 +154,22 @@ export class CommonCodeService {
}
}
/**
* ()
*/
async getCategoryByCode(categoryCode: string): Promise<CodeCategory | null> {
try {
const category = await queryOne<CodeCategory>(
`SELECT * FROM code_category WHERE category_code = $1`,
[categoryCode]
);
return category || null;
} catch (error) {
logger.error(`카테고리 조회 실패 (${categoryCode}):`, error);
return null;
}
}
/**
*
*/
@ -192,11 +214,12 @@ export class CommonCodeService {
}
// 회사별 필터링 (최고 관리자가 아닌 경우)
// company_code = '*'인 공통 데이터도 함께 조회
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`company_code = $${paramIndex}`);
whereConditions.push(`(company_code = $${paramIndex} OR company_code = '*')`);
values.push(userCompanyCode);
paramIndex++;
logger.info(`회사별 코드 필터링: ${userCompanyCode}`);
logger.info(`회사별 코드 필터링: ${userCompanyCode} (공통 데이터 포함)`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 코드 조회`);
}
@ -405,11 +428,22 @@ export class CommonCodeService {
menuObjid: number
) {
try {
// 계층 구조 깊이 계산
let depth = data.depth || 1;
if (data.parentCodeValue && !data.depth) {
// 부모 코드의 depth를 조회하여 +1
const parentCode = await queryOne<{ depth: number }>(
`SELECT depth FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
depth = (parentCode?.depth || 0) + 1;
}
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, NOW(), NOW())
is_active, menu_objid, company_code, parent_code_value, depth, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, $11, $12, NOW(), NOW())
RETURNING *`,
[
categoryCode,
@ -420,13 +454,15 @@ export class CommonCodeService {
data.sortOrder || 0,
menuObjid,
companyCode,
data.parentCodeValue || null,
depth,
createdBy,
createdBy,
]
);
logger.info(
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode})`
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode}, 부모: ${data.parentCodeValue || '없음'}, depth: ${depth})`
);
return code;
} catch (error) {
@ -491,6 +527,25 @@ export class CommonCodeService {
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
// 계층 구조 필드
if (data.parentCodeValue !== undefined) {
updateFields.push(`parent_code_value = $${paramIndex++}`);
values.push(data.parentCodeValue || null);
// depth 자동 계산
if (data.parentCodeValue) {
const parentCode = await queryOne<{ depth: number }>(
`SELECT depth FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
const newDepth = (parentCode?.depth || 0) + 1;
updateFields.push(`depth = $${paramIndex++}`);
values.push(newDepth);
} else {
updateFields.push(`depth = $${paramIndex++}`);
values.push(1); // 부모 없으면 최상위
}
}
// WHERE 절 구성
let whereClause = `WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}`;
@ -594,6 +649,164 @@ export class CommonCodeService {
}
}
/**
* ( )
*/
async getCodesHierarchy(
categoryCode: string,
userCompanyCode?: string
): Promise<any[]> {
try {
let sql = `
SELECT code_value, code_name, code_name_eng, description, sort_order,
is_active, parent_code_value, depth
FROM code_info
WHERE code_category = $1 AND is_active = 'Y'
`;
const values: any[] = [categoryCode];
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $2`;
values.push(userCompanyCode);
}
sql += ` ORDER BY depth ASC, sort_order ASC, code_value ASC`;
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
description: string | null;
sort_order: number;
is_active: string;
parent_code_value: string | null;
depth: number;
}>(sql, values);
// 트리 구조로 변환
const codeMap = new Map<string, any>();
const roots: any[] = [];
// 모든 코드를 맵에 저장
for (const code of codes) {
codeMap.set(code.code_value, {
value: code.code_value,
label: code.code_name,
labelEng: code.code_name_eng,
description: code.description,
depth: code.depth || 1,
parentValue: code.parent_code_value,
children: [],
});
}
// 부모-자식 관계 구성
for (const code of codes) {
const node = codeMap.get(code.code_value);
if (code.parent_code_value && codeMap.has(code.parent_code_value)) {
const parent = codeMap.get(code.parent_code_value);
parent.children.push(node);
} else {
roots.push(node);
}
}
logger.info(
`계층 코드 조회 완료: ${categoryCode} - ${roots.length}개 루트, 전체 ${codes.length}`
);
return roots;
} catch (error) {
logger.error(`계층 코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* ( )
*/
async getChildCodes(
categoryCode: string,
parentCodeValue: string | null,
userCompanyCode?: string
): Promise<Array<{ value: string; label: string; hasChildren: boolean }>> {
try {
let sql = `
SELECT
c.code_value,
c.code_name,
c.code_name_eng,
EXISTS(SELECT 1 FROM code_info c2 WHERE c2.parent_code_value = c.code_value AND c2.code_category = c.code_category) as has_children
FROM code_info c
WHERE c.code_category = $1
AND c.is_active = 'Y'
`;
const values: any[] = [categoryCode];
let paramIndex = 2;
// 부모 코드 필터
if (parentCodeValue) {
sql += ` AND c.parent_code_value = $${paramIndex++}`;
values.push(parentCodeValue);
} else {
sql += ` AND (c.parent_code_value IS NULL OR c.depth = 1)`;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND c.company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
sql += ` ORDER BY c.sort_order ASC, c.code_value ASC`;
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
has_children: boolean;
}>(sql, values);
const result = codes.map((code) => ({
value: code.code_value,
label: code.code_name,
hasChildren: code.has_children,
}));
logger.info(
`자식 코드 조회 완료: ${categoryCode} (부모: ${parentCodeValue || '최상위'}) - ${result.length}`
);
return result;
} catch (error) {
logger.error(`자식 코드 조회 중 오류 (${categoryCode}, ${parentCodeValue}):`, error);
throw error;
}
}
/**
* . ( )
*
* category code로
* `tableName_columnName` codeGroup으로
*
* @param tableName
* @param columnName
* @param userCompanyCode
*/
async getCategoryOptionsAsCode(tableName: string, columnName: string, userCompanyCode?: string) {
try {
// 카테고리 코드 그룹명 생성: TABLENAME_COLUMNNAME
const categoryCode = `${tableName.toUpperCase()}_${columnName.toUpperCase()}`;
logger.info(`카테고리 → 코드 호환 조회: ${tableName}.${columnName}${categoryCode}`);
return await this.getCodeOptions(categoryCode, userCompanyCode);
} catch (error) {
logger.error(`카테고리 호환 조회 중 오류 (${tableName}.${columnName}):`, error);
throw error;
}
}
/**
*
*/

View File

@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { ValidationMessage } from "@/components/common/ValidationMessage";
@ -83,6 +84,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
// 폼 스키마 선택 (생성/수정에 따라)
const schema = isEditing ? updateCodeSchema : createCodeSchema;
// 부모 코드 선택 상태
const [parentCodeValue, setParentCodeValue] = useState<string>("");
const form = useForm({
resolver: zodResolver(schema),
mode: "onChange", // 실시간 검증 활성화
@ -111,6 +115,9 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
// codeValue는 별도로 설정 (표시용)
form.setValue("codeValue" as any, editingCode.codeValue || editingCode.code_value);
// 부모 코드 설정
setParentCodeValue(editingCode.parentCodeValue || editingCode.parent_code_value || "");
} else {
// 새 코드 모드: 자동 순서 계산
const maxSortOrder = codes.length > 0 ? Math.max(...codes.map((c) => c.sortOrder || c.sort_order)) : 0;
@ -122,6 +129,7 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
description: "",
sortOrder: maxSortOrder + 1,
});
setParentCodeValue("");
}
}
}, [isOpen, isEditing, editingCode, codes]);
@ -129,22 +137,29 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
const handleSubmit = form.handleSubmit(async (data) => {
try {
if (isEditing && editingCode) {
// 수정
// 수정 - 부모 코드 포함
await updateCodeMutation.mutateAsync({
categoryCode,
codeValue: editingCode.codeValue || editingCode.code_value,
data: data as UpdateCodeData,
data: {
...data,
parentCodeValue: parentCodeValue || undefined,
} as UpdateCodeData,
});
} else {
// 생성
// 생성 - 부모 코드 포함
await createCodeMutation.mutateAsync({
categoryCode,
data: data as CreateCodeData,
data: {
...data,
parentCodeValue: parentCodeValue || undefined,
} as CreateCodeData,
});
}
onClose();
form.reset();
setParentCodeValue("");
} catch (error) {
console.error("코드 저장 실패:", error);
}
@ -269,6 +284,43 @@ export function CodeFormModal({ isOpen, onClose, categoryCode, editingCode, code
)}
</div>
{/* 부모 코드 (계층 구조) */}
<div className="space-y-2">
<Label htmlFor="parentCodeValue" className="text-xs sm:text-sm"> ()</Label>
<Select
value={parentCodeValue || "_none_"}
onValueChange={(val) => setParentCodeValue(val === "_none_" ? "" : val)}
disabled={isLoading}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="부모 코드 선택 (최상위면 비워두세요)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none_"> ()</SelectItem>
{codes
.filter((c) => {
// 자기 자신은 제외
const currentCodeValue = editingCode?.codeValue || editingCode?.code_value;
const codeValue = c.codeValue || c.code_value;
return codeValue !== currentCodeValue;
})
.map((c) => {
const codeValue = c.codeValue || c.code_value;
if (!codeValue) return null;
return (
<SelectItem key={codeValue} value={codeValue}>
{c.depth && c.depth > 1 ? "└".repeat(c.depth - 1) + " " : ""}
{c.codeName || c.code_name} ({codeValue})
</SelectItem>
);
})}
</SelectContent>
</Select>
<p className="text-[10px] sm:text-xs text-muted-foreground">
.
</p>
</div>
{/* 정렬 순서 */}
<div className="space-y-2">
<Label htmlFor="sortOrder" className="text-xs sm:text-sm"> </Label>

View File

@ -5,7 +5,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Edit, Trash2 } from "lucide-react";
import { Edit, Trash2, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { useUpdateCode } from "@/hooks/queries/useCodes";
import type { CodeInfo } from "@/types/commonCode";
@ -61,20 +61,32 @@ export function SortableCodeItem({
}
};
// 계층 깊이 계산 (들여쓰기용)
const depth = code.depth || 1;
const indentPx = (depth - 1) * 20; // 깊이당 20px 들여쓰기
return (
<div
ref={setNodeRef}
style={style}
style={{
...style,
marginLeft: `${indentPx}px`,
}}
{...attributes}
{...listeners}
className={cn(
"group cursor-grab rounded-lg border bg-card p-4 shadow-sm transition-all hover:shadow-md",
isDragging && "cursor-grabbing opacity-50",
depth > 1 && "border-l-2 border-l-primary/30",
)}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{/* 계층 표시 아이콘 */}
{depth > 1 && (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
<h4 className="text-sm font-semibold">{code.codeName || code.code_name}</h4>
<Badge
variant={code.isActive === "Y" || code.is_active === "Y" ? "default" : "secondary"}
@ -96,7 +108,14 @@ export function SortableCodeItem({
{code.isActive === "Y" || code.is_active === "Y" ? "활성" : "비활성"}
</Badge>
</div>
<p className="mt-1 text-xs text-muted-foreground">{code.codeValue || code.code_value}</p>
<p className="mt-1 text-xs text-muted-foreground">
{code.codeValue || code.code_value}
{code.parentCodeValue || code.parent_code_value ? (
<span className="ml-2 text-primary/70">
(: {code.parentCodeValue || code.parent_code_value})
</span>
) : null}
</p>
{code.description && <p className="mt-1 text-xs text-muted-foreground">{code.description}</p>}
</div>

View File

@ -12,7 +12,7 @@
* - swap: 스왑 ( )
*/
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
@ -26,6 +26,7 @@ import { cn } from "@/lib/utils";
import { UnifiedSelectProps, SelectOption } from "@/types/unified-components";
import { Check, ChevronsUpDown, X, ArrowLeftRight } from "lucide-react";
import { apiClient } from "@/lib/api/client";
import UnifiedFormContext from "./UnifiedFormContext";
/**
*
@ -463,20 +464,56 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
const [optionsLoaded, setOptionsLoaded] = useState(false);
// 옵션 로딩에 필요한 값들만 추출 (객체 참조 대신 원시값 사용)
const source = config.source;
// category 소스는 code로 자동 변환 (카테고리 → 공통코드 통합)
const rawSource = config.source;
const categoryTable = (config as any).categoryTable;
const categoryColumn = (config as any).categoryColumn;
// category 소스인 경우 code로 변환하고 codeGroup을 자동 생성
const source = rawSource === "category" ? "code" : rawSource;
const codeGroup = rawSource === "category" && categoryTable && categoryColumn
? `${categoryTable.toUpperCase()}_${categoryColumn.toUpperCase()}`
: config.codeGroup;
const entityTable = config.entityTable;
const entityValueColumn = config.entityValueColumn || config.entityValueField;
const entityLabelColumn = config.entityLabelColumn || config.entityLabelField;
const codeGroup = config.codeGroup;
const table = config.table;
const valueColumn = config.valueColumn;
const labelColumn = config.labelColumn;
const apiEndpoint = config.apiEndpoint;
const staticOptions = config.options;
// 계층 코드 연쇄 선택 관련
const hierarchical = config.hierarchical;
const parentField = config.parentField;
// FormContext에서 부모 필드 값 가져오기 (Context가 없으면 null)
const formContext = useContext(UnifiedFormContext);
// 부모 필드의 값 계산
const parentValue = useMemo(() => {
if (!hierarchical || !parentField) return null;
// FormContext가 있으면 거기서 값 가져오기
if (formContext) {
const val = formContext.getValue(parentField);
return val as string | null;
}
return null;
}, [hierarchical, parentField, formContext]);
// 데이터 소스에 따른 옵션 로딩 (원시값 의존성만 사용)
useEffect(() => {
// 이미 로드된 경우 스킵 (static 제외)
// 계층 구조인 경우 부모 값이 변경되면 다시 로드
if (hierarchical && source === "code") {
setOptionsLoaded(false);
}
}, [parentValue, hierarchical, source]);
useEffect(() => {
// 이미 로드된 경우 스킵 (static 제외, 계층 구조 제외)
if (optionsLoaded && source !== "static") {
return;
}
@ -493,7 +530,24 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
let fetchedOptions: SelectOption[] = [];
if (source === "code" && codeGroup) {
// 공통코드에서 로드
// 계층 구조 사용 시 자식 코드만 로드
if (hierarchical) {
const params = new URLSearchParams();
if (parentValue) {
params.append("parentCodeValue", parentValue);
}
const queryString = params.toString();
const url = `/common-codes/categories/${codeGroup}/children${queryString ? `?${queryString}` : ""}`;
const response = await apiClient.get(url);
const data = response.data;
if (data.success && data.data) {
fetchedOptions = data.data.map((item: { value: string; label: string; hasChildren: boolean }) => ({
value: item.value,
label: item.label,
}));
}
} else {
// 일반 공통코드에서 로드
const response = await apiClient.get(`/common-codes/${codeGroup}/items`);
const data = response.data;
if (data.success && data.data) {
@ -502,6 +556,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
label: item.codeName,
}));
}
}
} else if (source === "db" && table) {
// DB 테이블에서 로드
const response = await apiClient.get(`/entity/${table}/options`, {
@ -548,7 +603,7 @@ export const UnifiedSelect = forwardRef<HTMLDivElement, UnifiedSelectProps>(
};
loadOptions();
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded]);
}, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded, hierarchical, parentValue]);
// 모드별 컴포넌트 렌더링
const renderSelect = () => {

View File

@ -3,6 +3,8 @@ import {
CodeCategory,
CodeInfo,
CodeOption,
CodeHierarchyNode,
CodeChildOption,
CreateCategoryRequest,
UpdateCategoryRequest,
CreateCodeRequest,
@ -166,4 +168,34 @@ export const commonCodeApi = {
return response.data;
},
},
// 계층 구조 API
hierarchy: {
/**
* ( )
*/
async getTree(categoryCode: string): Promise<ApiResponse<CodeHierarchyNode[]>> {
const response = await apiClient.get(`/common-codes/categories/${categoryCode}/hierarchy`);
return response.data;
},
/**
* ( )
* @param categoryCode
* @param parentCodeValue (null이면 )
*/
async getChildren(
categoryCode: string,
parentCodeValue?: string | null
): Promise<ApiResponse<CodeChildOption[]>> {
const params = new URLSearchParams();
if (parentCodeValue) {
params.append("parentCodeValue", parentCodeValue);
}
const queryString = params.toString();
const url = `/common-codes/categories/${categoryCode}/children${queryString ? `?${queryString}` : ""}`;
const response = await apiClient.get(url);
return response.data;
},
},
};

View File

@ -25,6 +25,9 @@ export interface CodeInfo {
sortOrder?: number;
isActive?: string | boolean;
useYn?: string;
// 계층 구조 필드
parentCodeValue?: string | null;
depth?: number;
// 기존 필드 (하위 호환성을 위해 유지)
code_category?: string;
@ -33,6 +36,7 @@ export interface CodeInfo {
code_name_eng?: string | null;
sort_order?: number;
is_active?: string;
parent_code_value?: string | null;
created_date?: string | null;
created_by?: string | null;
updated_date?: string | null;
@ -61,6 +65,9 @@ export interface CreateCodeRequest {
codeNameEng?: string;
description?: string;
sortOrder?: number;
// 계층 구조 필드
parentCodeValue?: string;
depth?: number;
}
export interface UpdateCodeRequest {
@ -69,6 +76,8 @@ export interface UpdateCodeRequest {
description?: string;
sortOrder?: number;
isActive?: "Y" | "N"; // 백엔드에서 기대하는 문자열 타입
// 계층 구조 필드
parentCodeValue?: string;
}
export interface CodeOption {
@ -77,6 +86,24 @@ export interface CodeOption {
labelEng?: string | null;
}
// 계층 구조 트리 노드
export interface CodeHierarchyNode {
value: string;
label: string;
labelEng?: string | null;
description?: string | null;
depth: number;
parentValue?: string | null;
children: CodeHierarchyNode[];
}
// 자식 코드 (연쇄 선택용)
export interface CodeChildOption {
value: string;
label: string;
hasChildren: boolean;
}
export interface ReorderCodesRequest {
codes: Array<{
codeValue: string;

View File

@ -127,7 +127,7 @@ export interface UnifiedInputProps extends UnifiedBaseProps {
// ===== UnifiedSelect =====
export type UnifiedSelectMode = "dropdown" | "radio" | "check" | "tag" | "toggle" | "swap";
export type UnifiedSelectSource = "static" | "code" | "db" | "api" | "entity";
export type UnifiedSelectSource = "static" | "code" | "db" | "api" | "entity" | "category";
export interface SelectOption {
value: string;
@ -150,8 +150,13 @@ export interface UnifiedSelectConfig {
entityTable?: string;
entityValueField?: string;
entityLabelField?: string;
entityValueColumn?: string; // alias for entityValueField
entityLabelColumn?: string; // alias for entityLabelField
// API 연결 (source: api)
apiEndpoint?: string;
// 카테고리 연결 (source: category) - 레거시, code로 자동 변환됨
categoryTable?: string;
categoryColumn?: string;
// 공통 옵션
searchable?: boolean;
multiple?: boolean;
@ -161,6 +166,9 @@ export interface UnifiedSelectConfig {
cascading?: CascadingConfig;
// 상호 배제
mutualExclusion?: MutualExclusionConfig;
// 계층 코드 연쇄 선택 (source: code일 때 계층 구조 사용)
hierarchical?: boolean; // 계층 구조 사용 여부
parentField?: string; // 부모 값을 참조할 필드 (다른 컴포넌트의 columnName)
}
export interface UnifiedSelectProps extends UnifiedBaseProps {