Add environment variable example and update .gitignore
- Created a new .env.example file to provide a template for environment variables, including database connection details, JWT settings, encryption keys, and external API keys. - Updated .gitignore to include additional test output directories and archive files, ensuring that unnecessary files are not tracked by Git. - Removed outdated approval test reports and scripts that are no longer needed, streamlining the project structure. These changes improve the clarity of environment configuration and maintain a cleaner repository.
This commit is contained in:
@@ -79,10 +79,15 @@ export interface WIWorkItemDetail {
|
||||
unit?: string;
|
||||
lower_limit?: string;
|
||||
upper_limit?: string;
|
||||
base_value?: string;
|
||||
tolerance?: string;
|
||||
duration_minutes?: number;
|
||||
input_type?: string;
|
||||
lookup_target?: string;
|
||||
display_fields?: string;
|
||||
condition_base_value?: string;
|
||||
condition_tolerance?: string;
|
||||
condition_unit?: string;
|
||||
}
|
||||
|
||||
export interface WIWorkItem {
|
||||
|
||||
@@ -100,6 +100,38 @@ function ItemSearchModal({
|
||||
const [items, setItems] = useState<ItemInfo[]>([]);
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [catLabels, setCatLabels] = useState<Record<string, Record<string, string>>>({});
|
||||
|
||||
// item_info 카테고리 라벨 로드 (division, unit, type)
|
||||
useEffect(() => {
|
||||
const loadLabels = async () => {
|
||||
for (const col of ["division", "unit", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?includeInactive=true`);
|
||||
const vals = res.data?.data || [];
|
||||
if (vals.length > 0) {
|
||||
const map: Record<string, string> = {};
|
||||
vals.forEach((v: any) => { const code = v.valueCode || v.value_code; const label = v.valueLabel || v.value_label; if (code) map[code] = label; });
|
||||
setCatLabels((prev) => ({ ...prev, [col]: map }));
|
||||
}
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
};
|
||||
loadLabels();
|
||||
}, []);
|
||||
|
||||
const resolveCatLabel = (value: string, ...cols: string[]) => {
|
||||
if (!value) return "-";
|
||||
const resolve = (code: string) => {
|
||||
for (const col of cols) { if (catLabels[col]?.[code]) return catLabels[col][code]; }
|
||||
return code;
|
||||
};
|
||||
if (value.includes(",") || value.includes(";")) {
|
||||
const delim = value.includes(";") ? ";" : ",";
|
||||
return value.split(delim).map(s => resolve(s.trim())).filter(Boolean).join(", ");
|
||||
}
|
||||
return resolve(value);
|
||||
};
|
||||
|
||||
const searchItems = useCallback(
|
||||
async (query: string) => {
|
||||
@@ -244,8 +276,8 @@ function ItemSearchModal({
|
||||
{alreadyAdded && <span className="text-muted-foreground ml-1 text-[10px]">(추가됨)</span>}
|
||||
</td>
|
||||
<td className="px-3 py-2">{item.item_name}</td>
|
||||
<td className="px-3 py-2">{item.type}</td>
|
||||
<td className="px-3 py-2">{item.unit}</td>
|
||||
<td className="px-3 py-2">{resolveCatLabel(item.type || "", "division", "type")}</td>
|
||||
<td className="px-3 py-2">{resolveCatLabel(item.unit || "", "unit")}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -142,24 +142,35 @@ export function BomTreeComponent({
|
||||
const showHistory = features.showHistory !== false;
|
||||
const showVersion = features.showVersion !== false;
|
||||
|
||||
// 카테고리 라벨 캐시 (inputType === "category"인 모든 컬럼)
|
||||
// 카테고리 라벨 캐시
|
||||
const [categoryLabels, setCategoryLabels] = useState<Record<string, Record<string, string>>>({});
|
||||
useEffect(() => {
|
||||
const categoryColumns = displayColumns.filter((c) => c.inputType === "category");
|
||||
if (categoryColumns.length === 0) return;
|
||||
|
||||
const loadLabels = async () => {
|
||||
// inputType === "category"인 컬럼의 카테고리 로드
|
||||
const categoryColumns = displayColumns.filter((c) => c.inputType === "category");
|
||||
for (const col of categoryColumns) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/${detailTable}/${col.key}/values?includeInactive=true`);
|
||||
const vals = res.data?.data || [];
|
||||
if (vals.length > 0) {
|
||||
const map: Record<string, string> = {};
|
||||
vals.forEach((v: any) => { map[v.value_code] = v.value_label; });
|
||||
vals.forEach((v: any) => { const code = v.valueCode || v.value_code; const label = v.valueLabel || v.value_label; if (code) map[code] = label; });
|
||||
setCategoryLabels((prev) => ({ ...prev, [col.key]: map }));
|
||||
}
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
// item_info의 division/unit/type 카테고리 항상 로드 (BOM 헤더/상세의 구분/단위 컬럼용)
|
||||
for (const col of ["division", "unit", "type"]) {
|
||||
try {
|
||||
const res = await apiClient.get(`/table-categories/item_info/${col}/values?includeInactive=true`);
|
||||
const vals = res.data?.data || [];
|
||||
if (vals.length > 0) {
|
||||
const map: Record<string, string> = {};
|
||||
vals.forEach((v: any) => { const code = v.valueCode || v.value_code; const label = v.valueLabel || v.value_label; if (code) map[code] = label; });
|
||||
setCategoryLabels((prev) => ({ ...prev, [`item_${col}`]: map }));
|
||||
}
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
};
|
||||
loadLabels();
|
||||
}, [detailTable, displayColumns]);
|
||||
@@ -449,7 +460,21 @@ export function BomTreeComponent({
|
||||
|
||||
const getItemTypeLabel = (type: string) => {
|
||||
const map: Record<string, string> = { product: "제품", semi: "반제품", material: "원자재", part: "부품" };
|
||||
return map[type] || type || "-";
|
||||
if (map[type]) return map[type];
|
||||
// 카테고리 라벨에서 코드→라벨 변환 (division, type 모두 확인)
|
||||
const fromDiv = categoryLabels["item_division"]?.[type];
|
||||
if (fromDiv) return fromDiv;
|
||||
const fromType = categoryLabels["item_type"]?.[type];
|
||||
if (fromType) return fromType;
|
||||
// 콤마/세미콜론 구분 다중값인 경우 각각 변환
|
||||
if (type && (type.includes(";") || type.includes(","))) {
|
||||
const delimiter = type.includes(";") ? ";" : ",";
|
||||
return type.split(delimiter).map(t => {
|
||||
const trimmed = t.trim();
|
||||
return map[trimmed] || categoryLabels["item_division"]?.[trimmed] || categoryLabels["item_type"]?.[trimmed] || trimmed;
|
||||
}).join(", ");
|
||||
}
|
||||
return type || "-";
|
||||
};
|
||||
|
||||
const getItemTypeBadge = (type: string) => {
|
||||
@@ -545,14 +570,15 @@ export function BomTreeComponent({
|
||||
}
|
||||
|
||||
if (col.key === "unit") {
|
||||
const unitLabel = categoryLabels[col.key]?.[String(value)] || value;
|
||||
const unitLabel = categoryLabels[col.key]?.[String(value)] || categoryLabels["item_unit"]?.[String(value)] || value;
|
||||
return <span className="text-muted-foreground">{unitLabel || "-"}</span>;
|
||||
}
|
||||
|
||||
// fallback: 카테고리 라벨이 로드된 컬럼이면 라벨로 변환
|
||||
if (categoryLabels[col.key] && value) {
|
||||
const label = categoryLabels[col.key][String(value)] || String(value);
|
||||
return <span className="text-muted-foreground">{label || "-"}</span>;
|
||||
if (value) {
|
||||
const label = categoryLabels[col.key]?.[String(value)]
|
||||
|| categoryLabels[`item_${col.key}`]?.[String(value)];
|
||||
if (label) return <span className="text-muted-foreground">{label}</span>;
|
||||
}
|
||||
|
||||
return <span className="text-muted-foreground">{value ?? "-"}</span>;
|
||||
|
||||
+54
-19
@@ -1233,22 +1233,34 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
|
||||
const strValue = String(value);
|
||||
|
||||
if (mapping && mapping[strValue]) {
|
||||
const categoryData = mapping[strValue];
|
||||
return categoryData.label || strValue;
|
||||
}
|
||||
|
||||
// 전역 폴백: 컬럼명으로 매핑을 못 찾았을 때, 전체 매핑에서 값 검색
|
||||
if (!mapping && (strValue.startsWith("CAT_") || strValue.startsWith("CATEGORY_"))) {
|
||||
// 카테고리 코드 라벨 변환 헬퍼
|
||||
const resolveLabel = (code: string): string | null => {
|
||||
if (mapping && mapping[code]) return mapping[code].label || code;
|
||||
for (const key of Object.keys(categoryMappings)) {
|
||||
const m = categoryMappings[key];
|
||||
if (m && m[strValue]) {
|
||||
const categoryData = m[strValue];
|
||||
return categoryData.label || strValue;
|
||||
}
|
||||
if (m && m[code]) return m[code].label || code;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 콤마/세미콜론 구분 다중값 처리
|
||||
const looksLikeCatCode = (v: string) => v.startsWith("CAT_") || v.startsWith("CATEGORY_");
|
||||
if (looksLikeCatCode(strValue) || strValue.includes(",") || strValue.includes(";")) {
|
||||
const delimiter = strValue.includes(";") ? ";" : ",";
|
||||
const codes = strValue.includes(delimiter) ? strValue.split(delimiter).map(s => s.trim()).filter(Boolean) : [strValue];
|
||||
if (codes.some(c => looksLikeCatCode(c))) {
|
||||
const labels = codes.map(code => resolveLabel(code) || code);
|
||||
return labels.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
// 단일값 변환
|
||||
if (mapping && mapping[strValue]) {
|
||||
return mapping[strValue].label || strValue;
|
||||
}
|
||||
const resolved = resolveLabel(strValue);
|
||||
if (resolved) return resolved;
|
||||
|
||||
// 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체)
|
||||
if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) {
|
||||
return formatDateValue(value, "YYYY-MM-DD");
|
||||
@@ -2429,6 +2441,31 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}
|
||||
}
|
||||
|
||||
// 카테고리 타입이 아닌 컬럼 중 division/unit/type 등은 item_info에서 fallback 로드
|
||||
// 엔티티 조인 컬럼명은 "item_id_division" 형태이므로 끝부분으로 매칭
|
||||
const KNOWN_CAT_SUFFIXES = ["division", "unit", "type", "material"];
|
||||
const leftPanelCols = componentConfig.leftPanel?.columns || [];
|
||||
for (const col of leftPanelCols) {
|
||||
const colName = (col as any).name || (col as any).columnName || (col as any).column_name;
|
||||
if (!colName || mappings[colName]) continue;
|
||||
const suffix = KNOWN_CAT_SUFFIXES.find(s => colName === s || colName.endsWith(`_${s}`));
|
||||
if (!suffix) continue;
|
||||
try {
|
||||
const fbRes = await apiClient.get(`/table-categories/item_info/${suffix}/values?includeInactive=true`);
|
||||
if (fbRes.data.success && fbRes.data.data?.length > 0) {
|
||||
const fbMap: Record<string, { label: string; color?: string }> = {};
|
||||
const flatFb = (items: any[]) => {
|
||||
items.forEach((item: any) => {
|
||||
fbMap[item.value_code || item.valueCode] = { label: item.value_label || item.valueLabel, color: item.color };
|
||||
if (item.children?.length) flatFb(item.children);
|
||||
});
|
||||
};
|
||||
flatFb(fbRes.data.data);
|
||||
if (Object.keys(fbMap).length > 0) mappings[colName] = fbMap;
|
||||
}
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
|
||||
setLeftCategoryMappings(mappings);
|
||||
} catch (error) {
|
||||
console.error("좌측 카테고리 매핑 로드 실패:", error);
|
||||
@@ -4036,7 +4073,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-border divide-y bg-white">
|
||||
<tbody className="divide-border divide-y bg-card">
|
||||
<tr className="hover:bg-muted cursor-pointer">
|
||||
<td className="px-3 py-2 text-sm whitespace-nowrap">데이터 1-1</td>
|
||||
<td className="px-3 py-2 text-sm whitespace-nowrap">데이터 1-2</td>
|
||||
@@ -4155,7 +4192,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-border divide-y bg-white">
|
||||
<tbody className="divide-border divide-y bg-card">
|
||||
{group.items.map((item, idx) => {
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id";
|
||||
const itemId = item[sourceColumn] || item.id || item.ID || idx;
|
||||
@@ -4167,9 +4204,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
<tr
|
||||
key={itemId}
|
||||
onClick={() => handleLeftItemSelect(item)}
|
||||
className={`group hover:bg-accent cursor-pointer transition-colors ${
|
||||
isSelected ? "bg-primary/10" : ""
|
||||
}`}
|
||||
className={`group hover:bg-accent cursor-pointer transition-colors ${isSelected ? "bg-primary/10" : ""}`}
|
||||
>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
@@ -4190,7 +4225,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</td>
|
||||
))}
|
||||
{hasGroupedLeftActions && (
|
||||
<td className="bg-card group-hover:bg-accent sticky right-0 z-10 px-3 py-2 text-right">
|
||||
<td className={`sticky right-0 z-10 px-3 py-2 text-right ${isSelected ? "bg-transparent" : "bg-card"}`}>
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{componentConfig.leftPanel?.showEdit !== false && (
|
||||
<button
|
||||
@@ -4275,7 +4310,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-border divide-y bg-white">
|
||||
<tbody className="divide-border divide-y bg-card">
|
||||
{filteredData.map((item, idx) => {
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id";
|
||||
const itemId = item[sourceColumn] || item.id || item.ID || idx;
|
||||
@@ -4310,7 +4345,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</td>
|
||||
))}
|
||||
{hasLeftTableActions && (
|
||||
<td className="bg-card group-hover:bg-accent sticky right-0 z-10 px-3 py-2 text-right">
|
||||
<td className={`sticky right-0 z-10 px-3 py-2 text-right ${isSelected ? "bg-transparent" : "bg-card"}`}>
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{componentConfig.leftPanel?.showEdit !== false && (
|
||||
<button
|
||||
|
||||
@@ -1775,6 +1775,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
// 연쇄관계 매핑이 없는 경우 무시
|
||||
}
|
||||
|
||||
// 카테고리 타입이 아닌 컬럼 중 division/unit/type 등은 item_info에서 fallback 로드
|
||||
const KNOWN_CAT_COLS = ["division", "unit", "type", "material"];
|
||||
const allColNames = (tableConfig.columns || []).map((c: any) => c.columnName);
|
||||
for (const colName of allColNames) {
|
||||
if (mappings[colName]) continue;
|
||||
if (!KNOWN_CAT_COLS.includes(colName)) continue;
|
||||
try {
|
||||
const fbRes = await apiClient.get(`/table-categories/item_info/${colName}/values?includeInactive=true`);
|
||||
if (fbRes.data.success && fbRes.data.data?.length > 0) {
|
||||
const fbMapping: Record<string, { label: string; color?: string }> = {};
|
||||
flattenTree(fbRes.data.data, fbMapping);
|
||||
if (Object.keys(fbMapping).length > 0) mappings[colName] = fbMapping;
|
||||
}
|
||||
} catch { /* 무시 */ }
|
||||
}
|
||||
|
||||
setCategoryMappings(mappings);
|
||||
if (Object.keys(mappings).length > 0) {
|
||||
setCategoryMappingsKey((prev) => prev + 1);
|
||||
|
||||
Reference in New Issue
Block a user