feat: COMPANY_29 부서 등록 및 사용자 관리 페이지 개선

- 부서 등록 시 자동 생성 부서코드 기능 추가
- 사용자 관리에서 부서 필수 입력 검증 추가
- 품목 정보 페이지에서 입력 필드에 placeholder 추가
- 고객 관리 페이지에서 원본 카테고리 코드 보관 및 빈 문자열을 null로 변환하는 로직 추가
- 판매 주문 페이지에서 품목 검색 필터에 관리품목 선택 기능 추가

이 커밋은 부서 및 사용자 관리 기능을 개선하고, 사용자 경험을 향상시키기 위한 여러 변경 사항을 포함합니다.
This commit is contained in:
kjs 2026-03-29 20:04:52 +09:00
parent 3e935792d4
commit ac5292f9b0
17 changed files with 301 additions and 81 deletions

View File

@ -62,6 +62,7 @@ export class AuthController {
// 사용자의 첫 번째 접근 가능한 메뉴 조회
let firstMenuPath: string | null = null;
let firstMenuName: string | null = null;
try {
const menuList = await AdminService.getUserMenuList(paramMap);
logger.debug(`로그인 후 메뉴 조회: 총 ${menuList.length}개 메뉴`);
@ -74,7 +75,8 @@ export class AuthController {
if (firstMenu) {
firstMenuPath = firstMenu.menu_url || firstMenu.url;
logger.debug(`첫 번째 메뉴: ${firstMenuPath}`);
firstMenuName = firstMenu.menu_name_kor || firstMenu.translated_name || firstMenu.menu_name || null;
logger.debug(`첫 번째 메뉴: ${firstMenuPath} (${firstMenuName})`);
} else {
logger.debug("접근 가능한 메뉴 없음, 메인 페이지로 이동");
}
@ -112,6 +114,7 @@ export class AuthController {
userInfo,
token: loginResult.token,
firstMenuPath,
firstMenuName,
popLandingPath,
},
});

View File

@ -144,10 +144,35 @@ export default function DepartmentPage() {
useEffect(() => { fetchMembers(); }, [fetchMembers]);
// 부서 등록
const openDeptRegister = () => {
setDeptForm({});
const openDeptRegister = async () => {
setDeptForm({ dept_code: "불러오는 중..." });
setDeptEditMode(false);
setDeptModalOpen(true);
// DB에서 마지막 부서코드 조회 → 순번+1로 다음 코드 생성
try {
const res = await apiClient.post(`/table-management/tables/${DEPT_TABLE}/data`, {
page: 1, size: 500, autoFilter: true,
sort: { columnName: "dept_code", order: "desc" },
});
const rows = res.data?.data?.data || res.data?.data?.rows || [];
// 기존 부서코드에서 패턴 추출 (예: DEPT-001, DEPT-002 → 접두사 "DEPT", 구분자 "-", 순번 3자리)
const codes = rows.map((r: any) => r.dept_code).filter(Boolean).sort();
let nextCode = "(자동 생성)";
if (codes.length > 0) {
const lastCode = codes[codes.length - 1] as string;
// 마지막 숫자 부분 추출 (DEPT-003 → prefix="DEPT-", num=3, pad=3)
const match = lastCode.match(/^(.*?)(\d+)$/);
if (match) {
const prefix = match[1]; // "DEPT-"
const num = parseInt(match[2], 10) + 1;
const pad = match[2].length; // 3
nextCode = `${prefix}${String(num).padStart(pad, "0")}`;
}
}
setDeptForm((prev) => ({ ...prev, dept_code: nextCode }));
} catch {
setDeptForm((prev) => ({ ...prev, dept_code: "(자동 생성)" }));
}
};
const openDeptEdit = () => {
@ -169,7 +194,7 @@ export default function DepartmentPage() {
toast.success("수정되었습니다.");
} else {
await apiClient.post(`/table-management/tables/${DEPT_TABLE}/add`, {
dept_code: deptForm.dept_code || "",
dept_code: "",
dept_name: deptForm.dept_name,
parent_dept_code: deptForm.parent_dept_code || null,
});
@ -225,6 +250,7 @@ export default function DepartmentPage() {
const handleUserSave = async () => {
if (!userForm.user_id) { toast.error("사용자 ID는 필수입니다."); return; }
if (!userForm.user_name) { toast.error("사용자 이름은 필수입니다."); return; }
if (!userForm.dept_code) { toast.error("부서는 필수입니다."); return; }
const errors = validateForm(userForm, ["cell_phone", "email"]);
setFormErrors(errors);
if (Object.keys(errors).length > 0) { toast.error("입력 형식을 확인해주세요."); return; }
@ -240,10 +266,10 @@ export default function DepartmentPage() {
user_name: userForm.user_name,
user_name_eng: userForm.user_name_eng || undefined,
user_password: password || undefined,
email: userForm.email || undefined,
email: userEditMode ? (userForm.email || null) : (userForm.email || undefined),
tel: userForm.tel || undefined,
cell_phone: userForm.cell_phone || undefined,
sabun: userForm.sabun || undefined,
cell_phone: userEditMode ? (userForm.cell_phone || null) : (userForm.cell_phone || undefined),
sabun: userEditMode ? (userForm.sabun || null) : (userForm.sabun || undefined),
position_name: userForm.position_name || undefined,
dept_code: userForm.dept_code || undefined,
dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined,
@ -374,7 +400,7 @@ export default function DepartmentPage() {
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={deptForm.dept_code || ""} onChange={(e) => setDeptForm((p) => ({ ...p, dept_code: e.target.value }))}
placeholder="부서코드" className="h-9" disabled={deptEditMode} />
placeholder={deptEditMode ? "부서코드" : "자동 생성"} className="h-9" disabled />
</div>
<div className="space-y-1.5">
<Label className="text-sm"> <span className="text-destructive">*</span></Label>
@ -424,12 +450,12 @@ export default function DepartmentPage() {
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={userForm.sabun || ""} onChange={(e) => setUserForm((p) => ({ ...p, sabun: e.target.value }))}
placeholder="사번" className="h-9" />
placeholder="사번" className="h-9" autoComplete="off" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={userForm.user_password || ""} onChange={(e) => setUserForm((p) => ({ ...p, user_password: e.target.value }))}
placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" />
placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" autoComplete="new-password" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
@ -437,7 +463,7 @@ export default function DepartmentPage() {
placeholder="직급" className="h-9" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Label className="text-sm"> <span className="text-destructive">*</span></Label>
<Select value={userForm.dept_code || ""} onValueChange={(v) => setUserForm((p) => ({ ...p, dept_code: v }))}>
<SelectTrigger className="h-9"><SelectValue placeholder="부서 선택" /></SelectTrigger>
<SelectContent>

View File

@ -49,9 +49,9 @@ const FORM_FIELDS = [
{ key: "unit", label: "단위", type: "category" },
{ key: "material", label: "재질", type: "category" },
{ key: "status", label: "상태", type: "category" },
{ key: "weight", label: "중량", type: "text" },
{ key: "volum", label: "부피", type: "text" },
{ key: "specific_gravity", label: "비중", type: "text" },
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
{ key: "inventory_unit", label: "재고단위", type: "category" },
{ key: "selling_price", label: "판매가격", type: "text" },
{ key: "standard_price", label: "기준단가", type: "text" },
@ -483,7 +483,7 @@ export default function ItemInfoPage() {
<Input
value={formData[field.key] || ""}
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
placeholder={field.disabled ? field.placeholder : field.label}
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
disabled={field.disabled && !isEditMode}
className="h-9"
/>

View File

@ -81,6 +81,10 @@ export default function WorkInstructionPage() {
const [confirmWorker, setConfirmWorker] = useState("");
const [saving, setSaving] = useState(false);
// 등록 확인 모달 — 인라인 추가 폼
const [confirmAddQty, setConfirmAddQty] = useState("");
const [confirmAddWorkerOpen, setConfirmAddWorkerOpen] = useState(false);
// 수정 모달
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editOrder, setEditOrder] = useState<any>(null);
@ -217,6 +221,18 @@ export default function WorkInstructionPage() {
setIsRegModalOpen(false); setIsConfirmModalOpen(true);
};
// 등록 확인 모달 — 인라인 품목 추가
const addConfirmItem = () => {
if (!confirmAddQty || Number(confirmAddQty) <= 0) { alert("수량을 입력해주세요."); return; }
const firstItem = confirmItems[0];
setConfirmItems(prev => [...prev, {
itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "",
qty: Number(confirmAddQty), remark: "",
sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "",
}]);
setConfirmAddQty("");
};
// ─── 2단계 최종 적용 ───
const finalizeRegistration = async () => {
if (confirmItems.length === 0) { alert("품목이 없습니다."); return; }
@ -629,7 +645,7 @@ export default function WorkInstructionPage() {
<div className="max-h-[300px] overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow><TableHead className="w-[60px]"></TableHead><TableHead className="w-[120px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead></TableRow>
<TableRow><TableHead className="w-[60px]"></TableHead><TableHead className="w-[120px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[60px]" /></TableRow>
</TableHeader>
<TableBody>
{confirmItems.map((item, idx) => (
@ -640,6 +656,7 @@ export default function WorkInstructionPage() {
<TableCell className="text-xs">{item.spec || "-"}</TableCell>
<TableCell><Input type="number" className="h-7 text-xs w-20" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell><Input className="h-7 text-xs" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setConfirmItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}
</TableBody>
@ -711,19 +728,6 @@ export default function WorkInstructionPage() {
</div>
</div>
{/* 인라인 추가 폼 */}
<div className="border rounded-lg p-4 bg-muted/20">
<div className="flex items-end gap-3 flex-wrap">
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"> <span className="text-destructive">*</span></Label><Input type="number" value={addQty} onChange={e => setAddQty(e.target.value)} className="h-8 w-24 text-xs" placeholder="0" /></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label><Select value={nv(addEquipment)} onValueChange={v => setAddEquipment(fromNv(v))}><SelectTrigger className="h-8 w-[160px] text-xs"><SelectValue placeholder="설비 선택" /></SelectTrigger><SelectContent><SelectItem value="none"> </SelectItem>{equipmentOptions.map(eq => <SelectItem key={eq.id} value={eq.id}>{eq.equipment_name}</SelectItem>)}</SelectContent></Select></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label><Select value={nv(addWorkTeam)} onValueChange={v => setAddWorkTeam(fromNv(v))}><SelectTrigger className="h-8 w-[100px] text-xs"><SelectValue placeholder="작업조" /></SelectTrigger><SelectContent><SelectItem value="none"></SelectItem><SelectItem value="주간"></SelectItem><SelectItem value="야간"></SelectItem></SelectContent></Select></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label>
<div className="w-[150px]"><WorkerCombobox value={addWorker} onChange={setAddWorker} open={addWorkerOpen} onOpenChange={setAddWorkerOpen} triggerClassName="h-8 text-xs" /></div>
</div>
<Button size="sm" className="h-8" onClick={addEditItem}><Plus className="w-3 h-3 mr-1" /> </Button>
</div>
</div>
{/* 품목 테이블 */}
<div className="border rounded-lg overflow-hidden">
<div className="flex items-center justify-between p-3 bg-muted/20 border-b">

View File

@ -12,7 +12,7 @@
* - (delivery_destination)
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -56,6 +56,7 @@ const LEFT_COLUMNS: DataGridColumn[] = [
{ key: "contact_phone", label: "전화번호", width: "w-[110px]" },
{ key: "business_number", label: "사업자번호", width: "w-[110px]" },
{ key: "email", label: "이메일", width: "w-[130px]" },
{ key: "address", label: "주소", minWidth: "min-w-[150px]" },
{ key: "status", label: "상태", width: "w-[60px]" },
];
@ -79,6 +80,7 @@ export default function CustomerManagementPage() {
// 좌측: 거래처 목록
const [customers, setCustomers] = useState<any[]>([]);
const [rawCustomers, setRawCustomers] = useState<any[]>([]);
const [customerLoading, setCustomerLoading] = useState(false);
const [customerCount, setCustomerCount] = useState(0);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
@ -96,6 +98,7 @@ export default function CustomerManagementPage() {
// 품목 편집 데이터 (더블클릭 시 — 상세 입력 모달 재활용)
const [editItemData, setEditItemData] = useState<any>(null);
const savingRef = useRef(false);
const [deliveryLoading, setDeliveryLoading] = useState(false);
// 모달
@ -192,6 +195,8 @@ export default function CustomerManagementPage() {
autoFilter: true,
});
const raw = res.data?.data?.data || res.data?.data?.rows || [];
// raw 데이터 보관 (수정 시 원본 카테고리 코드 사용)
setRawCustomers(raw);
// 카테고리 코드→라벨 변환
const resolve = (col: string, code: string) => {
if (!code) return "";
@ -334,7 +339,9 @@ export default function CustomerManagementPage() {
const openCustomerEdit = () => {
if (!selectedCustomer) return;
setCustomerForm({ ...selectedCustomer });
// raw 데이터에서 원본 카테고리 코드 가져오기 (라벨 변환 전 데이터)
const rawData = rawCustomers.find((c) => c.id === selectedCustomerId);
setCustomerForm({ ...(rawData || selectedCustomer) });
setFormErrors({});
setCustomerEditMode(true);
setCustomerModalOpen(true);
@ -365,13 +372,18 @@ export default function CustomerManagementPage() {
setSaving(true);
try {
const { id, created_date, updated_date, writer, company_code, ...fields } = customerForm;
// 빈 문자열을 null로 변환 (DB 타입 호환)
const cleanFields: Record<string, any> = {};
for (const [key, value] of Object.entries(fields)) {
cleanFields[key] = value === "" ? null : value;
}
if (customerEditMode && id) {
await apiClient.put(`/table-management/tables/${CUSTOMER_TABLE}/edit`, {
originalData: { id }, updatedData: fields,
originalData: { id }, updatedData: cleanFields,
});
toast.success("수정되었습니다.");
} else {
await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/add`, fields);
await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/add`, cleanFields);
toast.success("등록되었습니다.");
}
setCustomerModalOpen(false);
@ -569,6 +581,8 @@ export default function CustomerManagementPage() {
const handleItemDetailSave = async () => {
if (!selectedCustomer) return;
if (savingRef.current) return;
savingRef.current = true;
const isEditingExisting = !!editItemData;
setSaving(true);
try {
@ -641,9 +655,16 @@ export default function CustomerManagementPage() {
});
}
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
const allPriceRows = itemPrices[itemKey] || [];
const priceRows = allPriceRows.filter((p) =>
(p.base_price && Number(p.base_price) > 0) || p.start_date
);
if (allPriceRows.length > 0 && priceRows.length === 0) {
toast.error("단가 정보를 입력해주세요. (기준가 또는 적용시작일 필수)");
setSaving(false);
savingRef.current = false;
return;
}
for (const price of priceRows) {
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
mapping_id: mappingId || "", customer_id: selectedCustomer.customer_code, item_id: itemKey,
@ -669,6 +690,7 @@ export default function CustomerManagementPage() {
toast.error(err.response?.data?.message || "저장에 실패했습니다.");
} finally {
setSaving(false);
savingRef.current = false;
}
};
@ -773,9 +795,10 @@ export default function CustomerManagementPage() {
// 셀렉트 렌더링
const renderSelect = (field: string, value: string, onChange: (v: string) => void, placeholder: string) => (
<Select value={value || ""} onValueChange={onChange}>
<Select value={value || "__none__"} onValueChange={(v) => onChange(v === "__none__" ? "" : v)}>
<SelectTrigger className="h-9"><SelectValue placeholder={placeholder} /></SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </SelectItem>
{(categoryOptions[field] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
</SelectContent>
</Select>

View File

@ -86,6 +86,7 @@ export default function SalesOrderPage() {
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
const [itemSearchLoading, setItemSearchLoading] = useState(false);
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
const [itemSearchDivision, setItemSearchDivision] = useState("all");
// 엑셀 업로드
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
@ -448,6 +449,17 @@ export default function SalesOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
if (itemSearchDivision !== "all") {
filters.push({ columnName: "division", operator: "equals", value: itemSearchDivision });
} else {
// 기본: 영업관련 division만 (판매품, 제품, 영업관리 등)
const salesDivCodes = (categoryOptions["item_division"] || [])
.filter((o) => ["판매품", "제품", "영업관리"].some((label) => o.label.includes(label)))
.map((o) => o.code);
if (salesDivCodes.length > 0) {
filters.push({ columnName: "division", operator: "in", value: salesDivCodes });
}
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 50,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@ -853,6 +865,15 @@ export default function SalesOrderPage() {
onChange={(e) => setItemSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchItems()}
className="h-9 flex-1" />
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button size="sm" onClick={searchItems} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> </>}
</Button>
@ -892,8 +913,8 @@ export default function SalesOrderPage() {
<TableCell className="text-xs max-w-[130px]"><span className="block truncate" title={item.item_number}>{item.item_number}</span></TableCell>
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
<TableCell className="text-xs">{item.size}</TableCell>
<TableCell className="text-xs">{item.material}</TableCell>
<TableCell className="text-xs">{item.unit}</TableCell>
<TableCell className="text-xs">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-xs">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow>
))}
</TableBody>

View File

@ -144,10 +144,35 @@ export default function DepartmentPage() {
useEffect(() => { fetchMembers(); }, [fetchMembers]);
// 부서 등록
const openDeptRegister = () => {
setDeptForm({});
const openDeptRegister = async () => {
setDeptForm({ dept_code: "불러오는 중..." });
setDeptEditMode(false);
setDeptModalOpen(true);
// DB에서 마지막 부서코드 조회 → 순번+1로 다음 코드 생성
try {
const res = await apiClient.post(`/table-management/tables/${DEPT_TABLE}/data`, {
page: 1, size: 500, autoFilter: true,
sort: { columnName: "dept_code", order: "desc" },
});
const rows = res.data?.data?.data || res.data?.data?.rows || [];
// 기존 부서코드에서 패턴 추출 (예: DEPT-001, DEPT-002 → 접두사 "DEPT", 구분자 "-", 순번 3자리)
const codes = rows.map((r: any) => r.dept_code).filter(Boolean).sort();
let nextCode = "(자동 생성)";
if (codes.length > 0) {
const lastCode = codes[codes.length - 1] as string;
// 마지막 숫자 부분 추출 (DEPT-003 → prefix="DEPT-", num=3, pad=3)
const match = lastCode.match(/^(.*?)(\d+)$/);
if (match) {
const prefix = match[1]; // "DEPT-"
const num = parseInt(match[2], 10) + 1;
const pad = match[2].length; // 3
nextCode = `${prefix}${String(num).padStart(pad, "0")}`;
}
}
setDeptForm((prev) => ({ ...prev, dept_code: nextCode }));
} catch {
setDeptForm((prev) => ({ ...prev, dept_code: "(자동 생성)" }));
}
};
const openDeptEdit = () => {
@ -169,7 +194,7 @@ export default function DepartmentPage() {
toast.success("수정되었습니다.");
} else {
await apiClient.post(`/table-management/tables/${DEPT_TABLE}/add`, {
dept_code: deptForm.dept_code || "",
dept_code: "",
dept_name: deptForm.dept_name,
parent_dept_code: deptForm.parent_dept_code || null,
});
@ -225,6 +250,7 @@ export default function DepartmentPage() {
const handleUserSave = async () => {
if (!userForm.user_id) { toast.error("사용자 ID는 필수입니다."); return; }
if (!userForm.user_name) { toast.error("사용자 이름은 필수입니다."); return; }
if (!userForm.dept_code) { toast.error("부서는 필수입니다."); return; }
const errors = validateForm(userForm, ["cell_phone", "email"]);
setFormErrors(errors);
if (Object.keys(errors).length > 0) { toast.error("입력 형식을 확인해주세요."); return; }
@ -240,10 +266,10 @@ export default function DepartmentPage() {
user_name: userForm.user_name,
user_name_eng: userForm.user_name_eng || undefined,
user_password: password || undefined,
email: userForm.email || undefined,
email: userEditMode ? (userForm.email || null) : (userForm.email || undefined),
tel: userForm.tel || undefined,
cell_phone: userForm.cell_phone || undefined,
sabun: userForm.sabun || undefined,
cell_phone: userEditMode ? (userForm.cell_phone || null) : (userForm.cell_phone || undefined),
sabun: userEditMode ? (userForm.sabun || null) : (userForm.sabun || undefined),
position_name: userForm.position_name || undefined,
dept_code: userForm.dept_code || undefined,
dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name || undefined,
@ -374,7 +400,7 @@ export default function DepartmentPage() {
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={deptForm.dept_code || ""} onChange={(e) => setDeptForm((p) => ({ ...p, dept_code: e.target.value }))}
placeholder="부서코드" className="h-9" disabled={deptEditMode} />
placeholder={deptEditMode ? "부서코드" : "자동 생성"} className="h-9" disabled />
</div>
<div className="space-y-1.5">
<Label className="text-sm"> <span className="text-destructive">*</span></Label>
@ -424,12 +450,12 @@ export default function DepartmentPage() {
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={userForm.sabun || ""} onChange={(e) => setUserForm((p) => ({ ...p, sabun: e.target.value }))}
placeholder="사번" className="h-9" />
placeholder="사번" className="h-9" autoComplete="off" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input value={userForm.user_password || ""} onChange={(e) => setUserForm((p) => ({ ...p, user_password: e.target.value }))}
placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" />
placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" autoComplete="new-password" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
@ -437,7 +463,7 @@ export default function DepartmentPage() {
placeholder="직급" className="h-9" />
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Label className="text-sm"> <span className="text-destructive">*</span></Label>
<Select value={userForm.dept_code || ""} onValueChange={(v) => setUserForm((p) => ({ ...p, dept_code: v }))}>
<SelectTrigger className="h-9"><SelectValue placeholder="부서 선택" /></SelectTrigger>
<SelectContent>

View File

@ -49,9 +49,9 @@ const FORM_FIELDS = [
{ key: "unit", label: "단위", type: "category" },
{ key: "material", label: "재질", type: "category" },
{ key: "status", label: "상태", type: "category" },
{ key: "weight", label: "중량", type: "text" },
{ key: "volum", label: "부피", type: "text" },
{ key: "specific_gravity", label: "비중", type: "text" },
{ key: "weight", label: "중량", type: "text", placeholder: "숫자 입력 (예: 3.5)" },
{ key: "volum", label: "부피", type: "text", placeholder: "숫자 입력 (예: 100)" },
{ key: "specific_gravity", label: "비중", type: "text", placeholder: "숫자 입력 (예: 7.85)" },
{ key: "inventory_unit", label: "재고단위", type: "category" },
{ key: "selling_price", label: "판매가격", type: "text" },
{ key: "standard_price", label: "기준단가", type: "text" },
@ -483,7 +483,7 @@ export default function ItemInfoPage() {
<Input
value={formData[field.key] || ""}
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
placeholder={field.disabled ? field.placeholder : field.label}
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
disabled={field.disabled && !isEditMode}
className="h-9"
/>

View File

@ -81,6 +81,10 @@ export default function WorkInstructionPage() {
const [confirmWorker, setConfirmWorker] = useState("");
const [saving, setSaving] = useState(false);
// 등록 확인 모달 — 인라인 추가 폼
const [confirmAddQty, setConfirmAddQty] = useState("");
const [confirmAddWorkerOpen, setConfirmAddWorkerOpen] = useState(false);
// 수정 모달
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editOrder, setEditOrder] = useState<any>(null);
@ -217,6 +221,18 @@ export default function WorkInstructionPage() {
setIsRegModalOpen(false); setIsConfirmModalOpen(true);
};
// 등록 확인 모달 — 인라인 품목 추가
const addConfirmItem = () => {
if (!confirmAddQty || Number(confirmAddQty) <= 0) { alert("수량을 입력해주세요."); return; }
const firstItem = confirmItems[0];
setConfirmItems(prev => [...prev, {
itemCode: firstItem?.itemCode || "", itemName: firstItem?.itemName || "", spec: firstItem?.spec || "",
qty: Number(confirmAddQty), remark: "",
sourceType: "item" as SourceType, sourceTable: "item_info", sourceId: firstItem?.itemCode || "",
}]);
setConfirmAddQty("");
};
// ─── 2단계 최종 적용 ───
const finalizeRegistration = async () => {
if (confirmItems.length === 0) { alert("품목이 없습니다."); return; }
@ -629,7 +645,7 @@ export default function WorkInstructionPage() {
<div className="max-h-[300px] overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-background z-10">
<TableRow><TableHead className="w-[60px]"></TableHead><TableHead className="w-[120px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead></TableRow>
<TableRow><TableHead className="w-[60px]"></TableHead><TableHead className="w-[120px]"></TableHead><TableHead></TableHead><TableHead className="w-[100px]"></TableHead><TableHead className="w-[100px]"></TableHead><TableHead></TableHead><TableHead className="w-[60px]" /></TableRow>
</TableHeader>
<TableBody>
{confirmItems.map((item, idx) => (
@ -640,6 +656,7 @@ export default function WorkInstructionPage() {
<TableCell className="text-xs">{item.spec || "-"}</TableCell>
<TableCell><Input type="number" className="h-7 text-xs w-20" value={item.qty} onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /></TableCell>
<TableCell><Input className="h-7 text-xs" value={item.remark} placeholder="비고" onChange={e => setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /></TableCell>
<TableCell><Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setConfirmItems(prev => prev.filter((_, i) => i !== idx))}><X className="w-3 h-3 text-destructive" /></Button></TableCell>
</TableRow>
))}
</TableBody>
@ -711,19 +728,6 @@ export default function WorkInstructionPage() {
</div>
</div>
{/* 인라인 추가 폼 */}
<div className="border rounded-lg p-4 bg-muted/20">
<div className="flex items-end gap-3 flex-wrap">
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"> <span className="text-destructive">*</span></Label><Input type="number" value={addQty} onChange={e => setAddQty(e.target.value)} className="h-8 w-24 text-xs" placeholder="0" /></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label><Select value={nv(addEquipment)} onValueChange={v => setAddEquipment(fromNv(v))}><SelectTrigger className="h-8 w-[160px] text-xs"><SelectValue placeholder="설비 선택" /></SelectTrigger><SelectContent><SelectItem value="none"> </SelectItem>{equipmentOptions.map(eq => <SelectItem key={eq.id} value={eq.id}>{eq.equipment_name}</SelectItem>)}</SelectContent></Select></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label><Select value={nv(addWorkTeam)} onValueChange={v => setAddWorkTeam(fromNv(v))}><SelectTrigger className="h-8 w-[100px] text-xs"><SelectValue placeholder="작업조" /></SelectTrigger><SelectContent><SelectItem value="none"></SelectItem><SelectItem value="주간"></SelectItem><SelectItem value="야간"></SelectItem></SelectContent></Select></div>
<div className="space-y-1"><Label className="text-[11px] text-muted-foreground"></Label>
<div className="w-[150px]"><WorkerCombobox value={addWorker} onChange={setAddWorker} open={addWorkerOpen} onOpenChange={setAddWorkerOpen} triggerClassName="h-8 text-xs" /></div>
</div>
<Button size="sm" className="h-8" onClick={addEditItem}><Plus className="w-3 h-3 mr-1" /> </Button>
</div>
</div>
{/* 품목 테이블 */}
<div className="border rounded-lg overflow-hidden">
<div className="flex items-center justify-between p-3 bg-muted/20 border-b">

View File

@ -12,7 +12,7 @@
* - (delivery_destination)
*/
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@ -56,6 +56,7 @@ const LEFT_COLUMNS: DataGridColumn[] = [
{ key: "contact_phone", label: "전화번호", width: "w-[110px]" },
{ key: "business_number", label: "사업자번호", width: "w-[110px]" },
{ key: "email", label: "이메일", width: "w-[130px]" },
{ key: "address", label: "주소", minWidth: "min-w-[150px]" },
{ key: "status", label: "상태", width: "w-[60px]" },
];
@ -79,6 +80,7 @@ export default function CustomerManagementPage() {
// 좌측: 거래처 목록
const [customers, setCustomers] = useState<any[]>([]);
const [rawCustomers, setRawCustomers] = useState<any[]>([]);
const [customerLoading, setCustomerLoading] = useState(false);
const [customerCount, setCustomerCount] = useState(0);
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
@ -96,6 +98,7 @@ export default function CustomerManagementPage() {
// 품목 편집 데이터 (더블클릭 시 — 상세 입력 모달 재활용)
const [editItemData, setEditItemData] = useState<any>(null);
const savingRef = useRef(false);
const [deliveryLoading, setDeliveryLoading] = useState(false);
// 모달
@ -192,6 +195,8 @@ export default function CustomerManagementPage() {
autoFilter: true,
});
const raw = res.data?.data?.data || res.data?.data?.rows || [];
// raw 데이터 보관 (수정 시 원본 카테고리 코드 사용)
setRawCustomers(raw);
// 카테고리 코드→라벨 변환
const resolve = (col: string, code: string) => {
if (!code) return "";
@ -334,7 +339,9 @@ export default function CustomerManagementPage() {
const openCustomerEdit = () => {
if (!selectedCustomer) return;
setCustomerForm({ ...selectedCustomer });
// raw 데이터에서 원본 카테고리 코드 가져오기 (라벨 변환 전 데이터)
const rawData = rawCustomers.find((c) => c.id === selectedCustomerId);
setCustomerForm({ ...(rawData || selectedCustomer) });
setFormErrors({});
setCustomerEditMode(true);
setCustomerModalOpen(true);
@ -365,13 +372,18 @@ export default function CustomerManagementPage() {
setSaving(true);
try {
const { id, created_date, updated_date, writer, company_code, ...fields } = customerForm;
// 빈 문자열을 null로 변환 (DB 타입 호환)
const cleanFields: Record<string, any> = {};
for (const [key, value] of Object.entries(fields)) {
cleanFields[key] = value === "" ? null : value;
}
if (customerEditMode && id) {
await apiClient.put(`/table-management/tables/${CUSTOMER_TABLE}/edit`, {
originalData: { id }, updatedData: fields,
originalData: { id }, updatedData: cleanFields,
});
toast.success("수정되었습니다.");
} else {
await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/add`, fields);
await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/add`, cleanFields);
toast.success("등록되었습니다.");
}
setCustomerModalOpen(false);
@ -569,6 +581,8 @@ export default function CustomerManagementPage() {
const handleItemDetailSave = async () => {
if (!selectedCustomer) return;
if (savingRef.current) return;
savingRef.current = true;
const isEditingExisting = !!editItemData;
setSaving(true);
try {
@ -641,9 +655,16 @@ export default function CustomerManagementPage() {
});
}
const priceRows = (itemPrices[itemKey] || []).filter((p) =>
const allPriceRows = itemPrices[itemKey] || [];
const priceRows = allPriceRows.filter((p) =>
(p.base_price && Number(p.base_price) > 0) || p.start_date
);
if (allPriceRows.length > 0 && priceRows.length === 0) {
toast.error("단가 정보를 입력해주세요. (기준가 또는 적용시작일 필수)");
setSaving(false);
savingRef.current = false;
return;
}
for (const price of priceRows) {
await apiClient.post(`/table-management/tables/${PRICE_TABLE}/add`, {
mapping_id: mappingId || "", customer_id: selectedCustomer.customer_code, item_id: itemKey,
@ -669,6 +690,7 @@ export default function CustomerManagementPage() {
toast.error(err.response?.data?.message || "저장에 실패했습니다.");
} finally {
setSaving(false);
savingRef.current = false;
}
};
@ -773,9 +795,10 @@ export default function CustomerManagementPage() {
// 셀렉트 렌더링
const renderSelect = (field: string, value: string, onChange: (v: string) => void, placeholder: string) => (
<Select value={value || ""} onValueChange={onChange}>
<Select value={value || "__none__"} onValueChange={(v) => onChange(v === "__none__" ? "" : v)}>
<SelectTrigger className="h-9"><SelectValue placeholder={placeholder} /></SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </SelectItem>
{(categoryOptions[field] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
</SelectContent>
</Select>

View File

@ -86,6 +86,7 @@ export default function SalesOrderPage() {
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
const [itemSearchLoading, setItemSearchLoading] = useState(false);
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
const [itemSearchDivision, setItemSearchDivision] = useState("all");
// 엑셀 업로드
const [excelUploadOpen, setExcelUploadOpen] = useState(false);
@ -448,6 +449,17 @@ export default function SalesOrderPage() {
if (itemSearchKeyword) {
filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
}
if (itemSearchDivision !== "all") {
filters.push({ columnName: "division", operator: "equals", value: itemSearchDivision });
} else {
// 기본: 영업관련 division만 (판매품, 제품, 영업관리 등)
const salesDivCodes = (categoryOptions["item_division"] || [])
.filter((o) => ["판매품", "제품", "영업관리"].some((label) => o.label.includes(label)))
.map((o) => o.code);
if (salesDivCodes.length > 0) {
filters.push({ columnName: "division", operator: "in", value: salesDivCodes });
}
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 50,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
@ -853,6 +865,15 @@ export default function SalesOrderPage() {
onChange={(e) => setItemSearchKeyword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchItems()}
className="h-9 flex-1" />
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Button size="sm" onClick={searchItems} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /> </>}
</Button>
@ -892,8 +913,8 @@ export default function SalesOrderPage() {
<TableCell className="text-xs max-w-[130px]"><span className="block truncate" title={item.item_number}>{item.item_number}</span></TableCell>
<TableCell className="text-sm max-w-[150px]"><span className="block truncate" title={item.item_name}>{item.item_name}</span></TableCell>
<TableCell className="text-xs">{item.size}</TableCell>
<TableCell className="text-xs">{item.material}</TableCell>
<TableCell className="text-xs">{item.unit}</TableCell>
<TableCell className="text-xs">{categoryOptions["item_material"]?.find((o) => o.code === item.material)?.label || item.material}</TableCell>
<TableCell className="text-xs">{categoryOptions["item_unit"]?.find((o) => o.code === item.unit)?.label || item.unit}</TableCell>
</TableRow>
))}
</TableBody>

View File

@ -114,9 +114,13 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp
}
logger.info("회사 전환 성공", { companyCode });
// 즉시 페이지 새로고침 (토큰이 이미 저장됨)
window.location.reload();
// 탭 스토어 초기화 + 메뉴명 캐시 제거
const { useTabStore } = await import("@/stores/tabStore");
useTabStore.getState().closeAllTabs();
localStorage.removeItem("currentMenuName");
// 메인 페이지로 이동 (이전 회사의 stale URL 방지)
window.location.href = "/";
} catch (error: any) {
logger.error("회사 전환 실패", error);
alert(error.message || "회사 전환 중 오류가 발생했습니다.");

View File

@ -119,6 +119,31 @@ const ADMIN_PAGE_REGISTRY: Record<string, React.ComponentType<any>> = {
// === COMPANY_9 (제일그라스) ===
"/COMPANY_9/sales/order": dynamic(() => import("@/app/(main)/COMPANY_9/sales/order/page"), { ssr: false, loading: LoadingFallback }),
// === COMPANY_29 ===
"/COMPANY_29/master-data/item-info": dynamic(() => import("@/app/(main)/COMPANY_29/master-data/item-info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/master-data/department": dynamic(() => import("@/app/(main)/COMPANY_29/master-data/department/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/order": dynamic(() => import("@/app/(main)/COMPANY_29/sales/order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/customer": dynamic(() => import("@/app/(main)/COMPANY_29/sales/customer/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/sales-item": dynamic(() => import("@/app/(main)/COMPANY_29/sales/sales-item/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/shipping-order": dynamic(() => import("@/app/(main)/COMPANY_29/sales/shipping-order/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/shipping-plan": dynamic(() => import("@/app/(main)/COMPANY_29/sales/shipping-plan/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/sales/claim": dynamic(() => import("@/app/(main)/COMPANY_29/sales/claim/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/production/process-info": dynamic(() => import("@/app/(main)/COMPANY_29/production/process-info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/production/work-instruction": dynamic(() => import("@/app/(main)/COMPANY_29/production/work-instruction/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/production/plan-management": dynamic(() => import("@/app/(main)/COMPANY_29/production/plan-management/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/equipment/info": dynamic(() => import("@/app/(main)/COMPANY_29/equipment/info/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/logistics/receiving": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/receiving/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/logistics/packaging": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/packaging/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/outsourcing/subcontractor": dynamic(() => import("@/app/(main)/COMPANY_29/outsourcing/subcontractor/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/outsourcing/subcontractor-item": dynamic(() => import("@/app/(main)/COMPANY_29/outsourcing/subcontractor-item/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/design/project": dynamic(() => import("@/app/(main)/COMPANY_29/design/project/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/design/change-management": dynamic(() => import("@/app/(main)/COMPANY_29/design/change-management/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/design/my-work": dynamic(() => import("@/app/(main)/COMPANY_29/design/my-work/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/design/design-request": dynamic(() => import("@/app/(main)/COMPANY_29/design/design-request/page"), { ssr: false, loading: LoadingFallback }),
"/COMPANY_29/design/task-management": dynamic(() => import("@/app/(main)/COMPANY_29/design/task-management/page"), { ssr: false, loading: LoadingFallback }),
"/admin/automaticMng/exconList": dynamic(() => import("@/app/(main)/admin/automaticMng/exconList/page"), { ssr: false, loading: LoadingFallback }),
"/admin/automaticMng/exCallConfList": dynamic(() => import("@/app/(main)/admin/automaticMng/exCallConfList/page"), { ssr: false, loading: LoadingFallback }),
@ -197,6 +222,30 @@ const DYNAMIC_ADMIN_IMPORTS: Record<string, () => Promise<any>> = {
"/COMPANY_7/design/task-management": () => import("@/app/(main)/COMPANY_7/design/task-management/page"),
// COMPANY_9 (제일그라스)
"/COMPANY_9/sales/order": () => import("@/app/(main)/COMPANY_9/sales/order/page"),
// COMPANY_29
"/COMPANY_29/master-data/item-info": () => import("@/app/(main)/COMPANY_29/master-data/item-info/page"),
"/COMPANY_29/master-data/department": () => import("@/app/(main)/COMPANY_29/master-data/department/page"),
"/COMPANY_29/sales/order": () => import("@/app/(main)/COMPANY_29/sales/order/page"),
"/COMPANY_29/sales/customer": () => import("@/app/(main)/COMPANY_29/sales/customer/page"),
"/COMPANY_29/sales/sales-item": () => import("@/app/(main)/COMPANY_29/sales/sales-item/page"),
"/COMPANY_29/sales/shipping-order": () => import("@/app/(main)/COMPANY_29/sales/shipping-order/page"),
"/COMPANY_29/sales/shipping-plan": () => import("@/app/(main)/COMPANY_29/sales/shipping-plan/page"),
"/COMPANY_29/sales/claim": () => import("@/app/(main)/COMPANY_29/sales/claim/page"),
"/COMPANY_29/production/process-info": () => import("@/app/(main)/COMPANY_29/production/process-info/page"),
"/COMPANY_29/production/work-instruction": () => import("@/app/(main)/COMPANY_29/production/work-instruction/page"),
"/COMPANY_29/production/plan-management": () => import("@/app/(main)/COMPANY_29/production/plan-management/page"),
"/COMPANY_29/equipment/info": () => import("@/app/(main)/COMPANY_29/equipment/info/page"),
"/COMPANY_29/logistics/material-status": () => import("@/app/(main)/COMPANY_29/logistics/material-status/page"),
"/COMPANY_29/logistics/outbound": () => import("@/app/(main)/COMPANY_29/logistics/outbound/page"),
"/COMPANY_29/logistics/receiving": () => import("@/app/(main)/COMPANY_29/logistics/receiving/page"),
"/COMPANY_29/logistics/packaging": () => import("@/app/(main)/COMPANY_29/logistics/packaging/page"),
"/COMPANY_29/outsourcing/subcontractor": () => import("@/app/(main)/COMPANY_29/outsourcing/subcontractor/page"),
"/COMPANY_29/outsourcing/subcontractor-item": () => import("@/app/(main)/COMPANY_29/outsourcing/subcontractor-item/page"),
"/COMPANY_29/design/project": () => import("@/app/(main)/COMPANY_29/design/project/page"),
"/COMPANY_29/design/change-management": () => import("@/app/(main)/COMPANY_29/design/change-management/page"),
"/COMPANY_29/design/my-work": () => import("@/app/(main)/COMPANY_29/design/my-work/page"),
"/COMPANY_29/design/design-request": () => import("@/app/(main)/COMPANY_29/design/design-request/page"),
"/COMPANY_29/design/task-management": () => import("@/app/(main)/COMPANY_29/design/task-management/page"),
};
const DYNAMIC_ADMIN_PATTERNS: Array<{

View File

@ -250,7 +250,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
if (screenMatch) {
const screenId = parseInt(screenMatch[1]);
const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined;
store.openTab({ type: "screen", title: `화면 ${screenId}`, screenId, menuObjid });
const savedMenuName = typeof window !== "undefined" ? localStorage.getItem("currentMenuName") : null;
store.openTab({ type: "screen", title: savedMenuName || `화면 ${screenId}`, screenId, menuObjid });
return;
}

View File

@ -115,6 +115,7 @@ export const useLogin = () => {
const result = await apiCall<{
token?: string;
firstMenuPath?: string;
firstMenuName?: string;
popLandingPath?: string;
}>("POST", AUTH_CONFIG.ENDPOINTS.LOGIN, {
userId: formData.userId,
@ -139,6 +140,10 @@ export const useLogin = () => {
}
} else {
const firstMenuPath = result.data?.firstMenuPath;
const firstMenuName = result.data?.firstMenuName;
if (firstMenuName) {
localStorage.setItem("currentMenuName", firstMenuName);
}
if (firstMenuPath) {
router.push(firstMenuPath);
} else {

View File

@ -38,7 +38,7 @@ interface SingleTableWithStickyProps {
onEditSave?: () => void;
editInputRef?: React.RefObject<HTMLInputElement | HTMLSelectElement>;
// 인라인 편집 타입별 옵션 (select/category/code, number, date 지원)
columnMeta?: Record<string, { inputType?: string }>;
columnMeta?: Record<string, { inputType?: string; detailSettings?: any }>;
categoryMappings?: Record<string, Record<string, { label: string }>>;
// 검색 하이라이트 관련 props
searchHighlights?: Set<string>;

View File

@ -873,7 +873,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }>
Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string; detailSettings?: any }>
>({});
// 🆕 엔티티 조인 테이블의 컬럼 메타데이터 (테이블명.컬럼명 → inputType)
const [joinedColumnMeta, setJoinedColumnMeta] = useState<
@ -1460,16 +1460,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const cached = tableColumnCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) {
const labels: Record<string, string> = {};
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }> = {};
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string; detailSettings?: any }> = {};
const inputTypeMap: Record<string, string> = {};
const categoryRefMap: Record<string, string> = {};
const detailSettingsMap: Record<string, any> = {};
if (cached.inputTypes) {
cached.inputTypes.forEach((col: any) => {
inputTypeMap[col.columnName] = col.inputType;
if (col.categoryRef) {
categoryRefMap[col.columnName] = col.categoryRef;
}
if (col.detailSettings && typeof col.detailSettings === "object") {
detailSettingsMap[col.columnName] = col.detailSettings;
}
});
}
@ -1480,6 +1484,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
codeCategory: col.codeCategory,
inputType: inputTypeMap[col.columnName],
categoryRef: categoryRefMap[col.columnName],
detailSettings: detailSettingsMap[col.columnName],
};
});
@ -1493,11 +1498,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const inputTypes = await tableTypeApi.getColumnInputTypes(tableConfig.selectedTable);
const inputTypeMap: Record<string, string> = {};
const categoryRefMap: Record<string, string> = {};
const detailSettingsMap: Record<string, any> = {};
inputTypes.forEach((col: any) => {
inputTypeMap[col.columnName] = col.inputType;
if (col.categoryRef) {
categoryRefMap[col.columnName] = col.categoryRef;
}
if (col.detailSettings && typeof col.detailSettings === "object") {
detailSettingsMap[col.columnName] = col.detailSettings;
}
});
tableColumnCache.set(cacheKey, {
@ -1507,7 +1516,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
const labels: Record<string, string> = {};
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string }> = {};
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string; categoryRef?: string; detailSettings?: any }> = {};
columns.forEach((col: any) => {
labels[col.columnName] = col.displayName || col.comment || col.columnName;
@ -1516,6 +1525,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
codeCategory: col.codeCategory,
inputType: inputTypeMap[col.columnName],
categoryRef: categoryRefMap[col.columnName],
detailSettings: detailSettingsMap[col.columnName],
};
});