feat: COMPANY_29 부서 등록 및 사용자 관리 페이지 개선
- 부서 등록 시 자동 생성 부서코드 기능 추가 - 사용자 관리에서 부서 필수 입력 검증 추가 - 품목 정보 페이지에서 입력 필드에 placeholder 추가 - 고객 관리 페이지에서 원본 카테고리 코드 보관 및 빈 문자열을 null로 변환하는 로직 추가 - 판매 주문 페이지에서 품목 검색 필터에 관리품목 선택 기능 추가 이 커밋은 부서 및 사용자 관리 기능을 개선하고, 사용자 경험을 향상시키기 위한 여러 변경 사항을 포함합니다.
This commit is contained in:
parent
3e935792d4
commit
ac5292f9b0
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 || "회사 전환 중 오류가 발생했습니다.");
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue