diff --git a/backend-node/src/controllers/packagingController.ts b/backend-node/src/controllers/packagingController.ts index face22f5..4974c80f 100644 --- a/backend-node/src/controllers/packagingController.ts +++ b/backend-node/src/controllers/packagingController.ts @@ -173,7 +173,11 @@ export async function getPkgUnitItems( const pool = getPool(); const result = await pool.query( - `SELECT * FROM pkg_unit_item WHERE pkg_code=$1 AND company_code=$2 ORDER BY created_date DESC`, + `SELECT pui.*, ii.item_name, ii.size AS spec, ii.unit + FROM pkg_unit_item pui + LEFT JOIN item_info ii ON pui.item_number = ii.item_number AND pui.company_code = ii.company_code + WHERE pui.pkg_code=$1 AND pui.company_code=$2 + ORDER BY pui.created_date DESC`, [pkgCode, companyCode] ); @@ -410,7 +414,11 @@ export async function getLoadingUnitPkgs( const pool = getPool(); const result = await pool.query( - `SELECT * FROM loading_unit_pkg WHERE loading_code=$1 AND company_code=$2 ORDER BY created_date DESC`, + `SELECT lup.*, pu.pkg_name, pu.pkg_type + FROM loading_unit_pkg lup + LEFT JOIN pkg_unit pu ON lup.pkg_code = pu.pkg_code AND lup.company_code = pu.company_code + WHERE lup.loading_code=$1 AND lup.company_code=$2 + ORDER BY lup.created_date DESC`, [loadingCode, companyCode] ); diff --git a/frontend/app/(main)/logistics/packaging/page.tsx b/frontend/app/(main)/logistics/packaging/page.tsx index 0b91e1ae..79059277 100644 --- a/frontend/app/(main)/logistics/packaging/page.tsx +++ b/frontend/app/(main)/logistics/packaging/page.tsx @@ -105,6 +105,7 @@ export default function PackagingPage() { const [pkgMatchQty, setPkgMatchQty] = useState(1); const [pkgMatchMethod, setPkgMatchMethod] = useState(""); const [pkgMatchSelected, setPkgMatchSelected] = useState(null); + const [pkgMatchSearchKw, setPkgMatchSearchKw] = useState(""); const [saving, setSaving] = useState(false); @@ -313,7 +314,7 @@ export default function PackagingPage() { // --- 포장단위 추가 모달 (적재함 구성) --- const openPkgMatchModal = () => { - setPkgMatchSelected(null); setPkgMatchQty(1); setPkgMatchMethod(""); + setPkgMatchSelected(null); setPkgMatchQty(1); setPkgMatchMethod(""); setPkgMatchSearchKw(""); setPkgMatchModalOpen(true); }; @@ -790,11 +791,19 @@ export default function PackagingPage() { 포장재에 매칭할 품목을 검색하여 추가합니다.
-
- setItemMatchKeyword(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && searchItemsForMatch()} className="h-9 text-xs" /> - -
+ { + setItemMatchKeyword(e.target.value); + const kw = e.target.value; + clearTimeout((window as any).__itemMatchTimer); + (window as any).__itemMatchTimer = setTimeout(async () => { + try { + const res = await getGeneralItems(kw || undefined); + if (res.success) setItemMatchResults(res.data); + } catch { /* ignore */ } + }, 300); + }} + className="h-9 text-xs" />
@@ -808,9 +817,9 @@ export default function PackagingPage() { - {itemMatchResults.length === 0 ? ( + {itemMatchResults.filter(i => !pkgItems.some(pi => pi.item_number === i.item_number)).length === 0 ? ( 검색 결과가 없습니다 - ) : itemMatchResults.map((item) => ( + ) : itemMatchResults.filter(i => !pkgItems.some(pi => pi.item_number === i.item_number)).map((item) => ( setItemMatchSelected(item)}> {itemMatchSelected?.id === item.id ? "✓" : ""} @@ -830,8 +839,8 @@ export default function PackagingPage() {
- - setItemMatchQty(Number(e.target.value) || 0)} min={1} className="h-9 text-xs" /> + + setItemMatchQty(Number(e.target.value) || 0)} min={1} className="h-9 text-xs" />
@@ -844,41 +853,59 @@ export default function PackagingPage() { {/* 포장단위 추가 모달 (적재함 구성) */} - + 포장단위 추가 — {selectedLoading?.loading_name} 적재함에 적재할 포장단위를 선택합니다.
-
+ setPkgMatchSearchKw(e.target.value)} + className="h-9 text-xs" + /> +
- 포장코드 + 포장코드 포장명 유형 + 크기(mm) + 최대중량 - {pkgUnits.length === 0 ? ( - 포장단위가 없습니다 - ) : pkgUnits.filter(p => p.status === "ACTIVE").map((p) => ( - setPkgMatchSelected(p)}> - {pkgMatchSelected?.id === p.id ? "✓" : ""} - {p.pkg_code} - {p.pkg_name} - {PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type} - - ))} + {(() => { + const kw = pkgMatchSearchKw.toLowerCase(); + const filtered = pkgUnits.filter(p => + p.status === "ACTIVE" + && !loadingPkgs.some(lp => lp.pkg_code === p.pkg_code) + && (!kw || p.pkg_code?.toLowerCase().includes(kw) || p.pkg_name?.toLowerCase().includes(kw)) + ); + return filtered.length === 0 ? ( + 추가 가능한 포장단위가 없습니다 + ) : filtered.map((p) => ( + setPkgMatchSelected(p)}> + {pkgMatchSelected?.id === p.id ? "✓" : ""} + {p.pkg_code} + {p.pkg_name} + {PKG_TYPE_LABEL[p.pkg_type] || p.pkg_type} + {fmtSize(p.width_mm, p.length_mm, p.height_mm)} + {Number(p.max_load_kg || 0) > 0 ? `${p.max_load_kg}kg` : "-"} + + )); + })()}
- - setPkgMatchQty(Number(e.target.value) || 0)} min={1} className="h-9 text-xs" /> + + setPkgMatchQty(Number(e.target.value) || 0)} min={1} className="h-9 text-xs" />
diff --git a/frontend/app/(main)/master-data/department/page.tsx b/frontend/app/(main)/master-data/department/page.tsx index 796f4ef0..09153b6e 100644 --- a/frontend/app/(main)/master-data/department/page.tsx +++ b/frontend/app/(main)/master-data/department/page.tsx @@ -63,7 +63,7 @@ export default function DepartmentPage() { const [deptLoading, setDeptLoading] = useState(false); const [deptCount, setDeptCount] = useState(0); const [searchFilters, setSearchFilters] = useState([]); - const [selectedDeptCode, setSelectedDeptCode] = useState(null); + const [selectedDeptId, setSelectedDeptId] = useState(null); // 우측: 사원 const [members, setMembers] = useState([]); @@ -77,6 +77,7 @@ export default function DepartmentPage() { // 사원 모달 const [userModalOpen, setUserModalOpen] = useState(false); + const [userEditMode, setUserEditMode] = useState(false); const [userForm, setUserForm] = useState>({}); const [formErrors, setFormErrors] = useState>({}); @@ -93,7 +94,9 @@ export default function DepartmentPage() { dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); - const data = res.data?.data?.data || res.data?.data?.rows || []; + const raw = res.data?.data?.data || res.data?.data?.rows || []; + // dept_info에 id 컬럼이 없으므로 dept_code를 id로 매핑 + const data = raw.map((d: any) => ({ ...d, id: d.id || d.dept_code })); setDepts(data); setDeptCount(res.data?.data?.total || data.length); } catch (err) { @@ -107,25 +110,27 @@ export default function DepartmentPage() { useEffect(() => { fetchDepts(); }, [fetchDepts]); // 선택된 부서 - const selectedDept = depts.find((d) => d.dept_code === selectedDeptCode); + const selectedDept = depts.find((d) => d.id === selectedDeptId); + const selectedDeptCode = selectedDept?.dept_code || null; - // 우측: 사원 조회 - useEffect(() => { - if (!selectedDeptCode) { setMembers([]); return; } - const fetchMembers = async () => { - setMemberLoading(true); - try { - const res = await apiClient.post(`/table-management/tables/${USER_TABLE}/data`, { - page: 1, size: 500, - dataFilter: { enabled: true, filters: [{ columnName: "dept_code", operator: "equals", value: selectedDeptCode }] }, - autoFilter: true, - }); - setMembers(res.data?.data?.data || res.data?.data?.rows || []); - } catch { setMembers([]); } finally { setMemberLoading(false); } - }; - fetchMembers(); + // 우측: 사원 조회 (부서 미선택 → 전체, 선택 → 해당 부서) + const fetchMembers = useCallback(async () => { + setMemberLoading(true); + try { + const filters = selectedDeptCode + ? [{ columnName: "dept_code", operator: "equals", value: selectedDeptCode }] + : []; + const res = await apiClient.post(`/table-management/tables/${USER_TABLE}/data`, { + page: 1, size: 500, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, + }); + setMembers(res.data?.data?.data || res.data?.data?.rows || []); + } catch { setMembers([]); } finally { setMemberLoading(false); } }, [selectedDeptCode]); + useEffect(() => { fetchMembers(); }, [fetchMembers]); + // 부서 등록 const openDeptRegister = () => { setDeptForm({}); @@ -180,14 +185,20 @@ export default function DepartmentPage() { data: [{ dept_code: selectedDeptCode }], }); toast.success("삭제되었습니다."); - setSelectedDeptCode(null); + setSelectedDeptId(null); fetchDepts(); } catch { toast.error("삭제에 실패했습니다."); } }; // 사원 추가 - const openUserModal = () => { - setUserForm({ dept_code: selectedDeptCode || "" }); + const openUserModal = (editData?: any) => { + if (editData) { + setUserEditMode(true); + setUserForm({ ...editData, user_password: "" }); + } else { + setUserEditMode(false); + setUserForm({ dept_code: selectedDeptCode || "", user_password: "" }); + } setFormErrors({}); setUserModalOpen(true); }; @@ -208,14 +219,34 @@ export default function DepartmentPage() { setSaving(true); try { - const { created_date, updated_date, ...fields } = userForm; - await apiClient.post(`/table-management/tables/${USER_TABLE}/add`, fields); - toast.success("사원이 추가되었습니다."); + // 비밀번호 미입력 시 기본값 (신규만) + const password = userForm.user_password || (!userEditMode ? "qlalfqjsgh11" : undefined); + + await apiClient.post("/admin/users/with-dept", { + userInfo: { + user_id: userForm.user_id, + user_name: userForm.user_name, + user_name_eng: userForm.user_name_eng || undefined, + user_password: password || undefined, + email: userForm.email || undefined, + tel: userForm.tel || undefined, + cell_phone: userForm.cell_phone || undefined, + sabun: 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, + status: userForm.status || "active", + }, + mainDept: userForm.dept_code ? { + dept_code: userForm.dept_code, + dept_name: depts.find((d) => d.dept_code === userForm.dept_code)?.dept_name, + position_name: userForm.position_name || undefined, + } : undefined, + isUpdate: userEditMode, + }); + toast.success(userEditMode ? "사원 정보가 수정되었습니다." : "사원이 추가되었습니다."); setUserModalOpen(false); - // 우측 새로고침 - const code = selectedDeptCode; - setSelectedDeptCode(null); - setTimeout(() => setSelectedDeptCode(code), 50); + fetchMembers(); } catch (err: any) { toast.error(err.response?.data?.message || "저장에 실패했습니다."); } finally { @@ -275,10 +306,9 @@ export default function DepartmentPage() { columns={LEFT_COLUMNS} data={depts} loading={deptLoading} - selectedId={selectedDeptCode} + selectedId={selectedDeptId} onSelect={(id) => { - const dept = depts.find((d) => d.dept_code === id || d.id === id); - setSelectedDeptCode(dept?.dept_code || id); + setSelectedDeptId((prev) => (prev === id ? null : id)); }} onRowDoubleClick={() => openDeptEdit()} emptyMessage="등록된 부서가 없습니다" @@ -293,29 +323,25 @@ export default function DepartmentPage() {
- 부서 인원 + + {selectedDept ? "부서 인원" : "전체 사원"} {selectedDept && {selectedDept.dept_name}} {members.length > 0 && {members.length}명}
-
- {!selectedDeptCode ? ( -
- 좌측에서 부서를 선택하세요 -
- ) : ( - - )} + openUserModal(row)} + />
@@ -365,14 +391,14 @@ export default function DepartmentPage() { - 사원 추가 - {selectedDept?.dept_name} 부서에 사원을 추가합니다. + {userEditMode ? "사원 정보 수정" : "사원 추가"} + {userEditMode ? `${userForm.user_name} (${userForm.user_id}) 사원 정보를 수정합니다.` : selectedDept ? `${selectedDept.dept_name} 부서에 사원을 추가합니다.` : "사원을 추가합니다."}
setUserForm((p) => ({ ...p, user_id: e.target.value }))} - placeholder="사용자 ID" className="h-9" /> + placeholder="사용자 ID" className="h-9" disabled={userEditMode} />
@@ -387,7 +413,7 @@ export default function DepartmentPage() {
setUserForm((p) => ({ ...p, user_password: e.target.value }))} - placeholder="비밀번호" className="h-9" type="password" /> + placeholder={userEditMode ? "변경 시에만 입력" : "미입력 시 qlalfqjsgh11"} className="h-9" type="password" />