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
@@ -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>