refactor: Update item number handling and improve sales order filtering
- Changed item number field type from 'text' to 'numbering' in item info forms for better clarity. - Enhanced the logic for loading numbering previews, including handling manual input values and category changes. - Updated sales order page to filter items based on customer pricing rules, ensuring only relevant items are displayed. - Improved date handling in sales order page to ensure consistent formatting. These changes aim to enhance the user experience and data integrity across the application.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function SalesOrderPage() {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` }));
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
@@ -608,6 +608,24 @@ export default function SalesOrderPage() {
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
|
||||
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
|
||||
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
|
||||
const partnerId = masterForm.partner_id;
|
||||
let customerItemIds: Set<string> | null = null;
|
||||
|
||||
if (isCustomerPrice && partnerId) {
|
||||
try {
|
||||
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
|
||||
autoFilter: true,
|
||||
});
|
||||
const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || [];
|
||||
customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean));
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
@@ -615,6 +633,12 @@ export default function SalesOrderPage() {
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
let allRows = resData?.data || resData?.rows || [];
|
||||
|
||||
// 거래처우선일 때 연결된 품목만 표시
|
||||
if (customerItemIds) {
|
||||
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
|
||||
}
|
||||
|
||||
// 관리품목 필터 (코드/라벨 혼재 대응)
|
||||
if (itemSearchDivision !== "all") {
|
||||
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
|
||||
@@ -678,7 +702,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
for (const p of prices) {
|
||||
const start = p.start_date || "";
|
||||
const end = p.end_date || "";
|
||||
@@ -768,7 +793,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
const priceMap: Record<string, string> = {};
|
||||
for (const p of prices) {
|
||||
if (p.start_date && p.start_date > today) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function SalesOrderPage() {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` }));
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
@@ -608,6 +608,24 @@ export default function SalesOrderPage() {
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
|
||||
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
|
||||
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
|
||||
const partnerId = masterForm.partner_id;
|
||||
let customerItemIds: Set<string> | null = null;
|
||||
|
||||
if (isCustomerPrice && partnerId) {
|
||||
try {
|
||||
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
|
||||
autoFilter: true,
|
||||
});
|
||||
const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || [];
|
||||
customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean));
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
@@ -615,6 +633,12 @@ export default function SalesOrderPage() {
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
let allRows = resData?.data || resData?.rows || [];
|
||||
|
||||
// 거래처우선일 때 연결된 품목만 표시
|
||||
if (customerItemIds) {
|
||||
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
|
||||
}
|
||||
|
||||
// 관리품목 필터 (코드/라벨 혼재 대응)
|
||||
if (itemSearchDivision !== "all") {
|
||||
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
|
||||
@@ -678,7 +702,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
for (const p of prices) {
|
||||
const start = p.start_date || "";
|
||||
const end = p.end_date || "";
|
||||
@@ -768,7 +793,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
const priceMap: Record<string, string> = {};
|
||||
for (const p of prices) {
|
||||
if (p.start_date && p.start_date > today) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function SalesOrderPage() {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` }));
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
@@ -608,6 +608,24 @@ export default function SalesOrderPage() {
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
|
||||
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
|
||||
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
|
||||
const partnerId = masterForm.partner_id;
|
||||
let customerItemIds: Set<string> | null = null;
|
||||
|
||||
if (isCustomerPrice && partnerId) {
|
||||
try {
|
||||
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
|
||||
autoFilter: true,
|
||||
});
|
||||
const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || [];
|
||||
customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean));
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
@@ -615,6 +633,12 @@ export default function SalesOrderPage() {
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
let allRows = resData?.data || resData?.rows || [];
|
||||
|
||||
// 거래처우선일 때 연결된 품목만 표시
|
||||
if (customerItemIds) {
|
||||
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
|
||||
}
|
||||
|
||||
// 관리품목 필터 (코드/라벨 혼재 대응)
|
||||
if (itemSearchDivision !== "all") {
|
||||
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
|
||||
@@ -678,7 +702,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
for (const p of prices) {
|
||||
const start = p.start_date || "";
|
||||
const end = p.end_date || "";
|
||||
@@ -768,7 +793,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
const priceMap: Record<string, string> = {};
|
||||
for (const p of prices) {
|
||||
if (p.start_date && p.start_date > today) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function JeilGlassOrderPage() {
|
||||
const custs = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({
|
||||
code: c.customer_code,
|
||||
label: `${c.customer_name} (${c.customer_code})`,
|
||||
label: c.customer_name,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// 담당자
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function SalesOrderPage() {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` }));
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
@@ -608,6 +608,24 @@ export default function SalesOrderPage() {
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
|
||||
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
|
||||
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
|
||||
const partnerId = masterForm.partner_id;
|
||||
let customerItemIds: Set<string> | null = null;
|
||||
|
||||
if (isCustomerPrice && partnerId) {
|
||||
try {
|
||||
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
|
||||
autoFilter: true,
|
||||
});
|
||||
const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || [];
|
||||
customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean));
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
@@ -615,6 +633,12 @@ export default function SalesOrderPage() {
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
let allRows = resData?.data || resData?.rows || [];
|
||||
|
||||
// 거래처우선일 때 연결된 품목만 표시
|
||||
if (customerItemIds) {
|
||||
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
|
||||
}
|
||||
|
||||
// 관리품목 필터 (코드/라벨 혼재 대응)
|
||||
if (itemSearchDivision !== "all") {
|
||||
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
|
||||
@@ -678,7 +702,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
for (const p of prices) {
|
||||
const start = p.start_date || "";
|
||||
const end = p.end_date || "";
|
||||
@@ -768,7 +793,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
const priceMap: Record<string, string> = {};
|
||||
for (const p of prices) {
|
||||
if (p.start_date && p.start_date > today) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -249,7 +249,7 @@ export default function SalesOrderPage() {
|
||||
page: 1, size: 500, autoFilter: true,
|
||||
});
|
||||
const custs = custRes.data?.data?.data || custRes.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: `${c.customer_name} (${c.customer_code})` }));
|
||||
optMap["partner_id"] = custs.map((c: any) => ({ code: c.customer_code, label: c.customer_name }));
|
||||
} catch { /* skip */ }
|
||||
// 사용자 목록
|
||||
try {
|
||||
@@ -608,6 +608,24 @@ export default function SalesOrderPage() {
|
||||
try {
|
||||
const filters: any[] = [];
|
||||
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
|
||||
|
||||
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
|
||||
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
|
||||
const partnerId = masterForm.partner_id;
|
||||
let customerItemIds: Set<string> | null = null;
|
||||
|
||||
if (isCustomerPrice && partnerId) {
|
||||
try {
|
||||
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
|
||||
autoFilter: true,
|
||||
});
|
||||
const mappings = mappingRes.data?.data?.data || mappingRes.data?.data?.rows || [];
|
||||
customerItemIds = new Set(mappings.map((m: any) => m.item_id).filter(Boolean));
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
||||
page: 1, size: 500,
|
||||
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
||||
@@ -615,6 +633,12 @@ export default function SalesOrderPage() {
|
||||
});
|
||||
const resData = res.data?.data;
|
||||
let allRows = resData?.data || resData?.rows || [];
|
||||
|
||||
// 거래처우선일 때 연결된 품목만 표시
|
||||
if (customerItemIds) {
|
||||
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
|
||||
}
|
||||
|
||||
// 관리품목 필터 (코드/라벨 혼재 대응)
|
||||
if (itemSearchDivision !== "all") {
|
||||
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
|
||||
@@ -678,7 +702,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
for (const p of prices) {
|
||||
const start = p.start_date || "";
|
||||
const end = p.end_date || "";
|
||||
@@ -768,7 +793,8 @@ export default function SalesOrderPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const prices = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const _n = new Date();
|
||||
const today = `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`;
|
||||
const priceMap: Record<string, string> = {};
|
||||
for (const p of prices) {
|
||||
if (p.start_date && p.start_date > today) continue;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
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 { Label } from "@/components/ui/label";
|
||||
@@ -156,7 +156,7 @@ const GRID_COLUMNS = [
|
||||
];
|
||||
|
||||
const FORM_FIELDS = [
|
||||
{ key: "item_number", label: "품목코드", type: "text", required: true, disabled: true, placeholder: "자동 채번" },
|
||||
{ key: "item_number", label: "품목코드", type: "numbering", required: true, placeholder: "자동 채번" },
|
||||
{ key: "item_name", label: "품명", type: "text", required: true },
|
||||
{ key: "division", label: "관리품목", type: "multi-category" },
|
||||
{ key: "type", label: "품목구분", type: "category" },
|
||||
@@ -209,6 +209,13 @@ export default function ItemInfoPage() {
|
||||
// 선택된 행
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 채번 관련 상태
|
||||
const [numberingRule, setNumberingRule] = useState<any>(null);
|
||||
const [numberingTemplate, setNumberingTemplate] = useState<string>("");
|
||||
const [manualInputValue, setManualInputValue] = useState<string>("");
|
||||
const [isNumberingLoading, setIsNumberingLoading] = useState(false);
|
||||
const numberingRuleIdRef = useRef<string | null>(null);
|
||||
|
||||
// 카테고리 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCategories = async () => {
|
||||
@@ -288,26 +295,49 @@ export default function ItemInfoPage() {
|
||||
}, [fetchItems]);
|
||||
|
||||
// 채번 미리보기 로드
|
||||
const loadNumberingPreview = async () => {
|
||||
const loadNumberingPreview = async (currentFormData?: Record<string, any>, currentManualValue?: string) => {
|
||||
try {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
const rule = ruleRes.data?.data;
|
||||
if (rule?.ruleId) {
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, { formData: {} });
|
||||
return previewRes.data?.data?.generatedCode || "";
|
||||
setIsNumberingLoading(true);
|
||||
|
||||
// 규칙 조회 (캐싱)
|
||||
let rule = numberingRule;
|
||||
if (!rule) {
|
||||
const ruleRes = await apiClient.get(`/numbering-rules/by-column/${TABLE_NAME}/item_number`);
|
||||
rule = ruleRes.data?.data;
|
||||
if (rule) {
|
||||
setNumberingRule(rule);
|
||||
numberingRuleIdRef.current = rule.ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rule?.ruleId) return "";
|
||||
|
||||
// preview 호출 (formData + manualInputValue 전달)
|
||||
const previewRes = await apiClient.post(`/numbering-rules/${rule.ruleId}/preview`, {
|
||||
formData: currentFormData || {},
|
||||
manualInputValue: currentManualValue || undefined,
|
||||
});
|
||||
|
||||
const generatedCode = previewRes.data?.data?.generatedCode || "";
|
||||
setNumberingTemplate(generatedCode);
|
||||
return generatedCode;
|
||||
} catch { /* 채번 규칙 없으면 무시 */ }
|
||||
finally {
|
||||
setIsNumberingLoading(false);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 등록 모달 열기
|
||||
const openRegisterModal = async () => {
|
||||
setFormData({});
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
setIsModalOpen(true);
|
||||
// 채번 컬럼 자동 로드
|
||||
const code = await loadNumberingPreview();
|
||||
// 채번 미리보기
|
||||
const code = await loadNumberingPreview({});
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
@@ -315,6 +345,8 @@ export default function ItemInfoPage() {
|
||||
const openEditModal = (item: any) => {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
setFormData({ ...raw });
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(true);
|
||||
setEditId(item.id);
|
||||
setIsModalOpen(true);
|
||||
@@ -325,13 +357,72 @@ export default function ItemInfoPage() {
|
||||
const raw = rawItems.find((r) => r.id === item.id) || item;
|
||||
const { id, item_number, created_date, updated_date, writer, ...rest } = raw;
|
||||
setFormData(rest);
|
||||
setManualInputValue("");
|
||||
setNumberingTemplate("");
|
||||
setIsEditMode(false);
|
||||
setEditId(null);
|
||||
const code = await loadNumberingPreview();
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
setIsModalOpen(true);
|
||||
// 복사된 formData 기반으로 preview
|
||||
const code = await loadNumberingPreview(rest);
|
||||
if (code) setFormData(prev => ({ ...prev, item_number: code }));
|
||||
};
|
||||
|
||||
// 카테고리 변경 시 채번 preview 재호출
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
|
||||
const hasCategoryPart = numberingRule?.parts?.some(
|
||||
(p: any) => p.partType === "category" && p.generationMethod === "auto"
|
||||
);
|
||||
if (!hasCategoryPart) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const code = await loadNumberingPreview(formData, manualInputValue);
|
||||
if (code) {
|
||||
if (code.includes("____")) {
|
||||
setNumberingTemplate(code);
|
||||
const parts = code.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, item_number: code }));
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...CATEGORY_COLUMNS.map(col => formData[col])]);
|
||||
|
||||
// 수동 입력값 변경 시 preview 갱신 (순번 재계산)
|
||||
useEffect(() => {
|
||||
if (isEditMode || !isModalOpen || !numberingRuleIdRef.current) return;
|
||||
if (!numberingTemplate.includes("____")) return;
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const previewRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/preview`,
|
||||
{ formData, manualInputValue: manualInputValue || undefined }
|
||||
);
|
||||
const newCode = previewRes.data?.data?.generatedCode || "";
|
||||
if (newCode) {
|
||||
setNumberingTemplate(newCode);
|
||||
if (newCode.includes("____")) {
|
||||
const parts = newCode.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
setFormData(prev => ({ ...prev, item_number: prefix + manualInputValue + suffix }));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [manualInputValue]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!formData.item_name) {
|
||||
@@ -342,6 +433,7 @@ export default function ItemInfoPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEditMode && editId) {
|
||||
// 수정: item_number는 변경하지 않음
|
||||
const { id, created_date, updated_date, writer, company_code, ...updateFields } = formData;
|
||||
await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, {
|
||||
originalData: { id: editId },
|
||||
@@ -349,8 +441,42 @@ export default function ItemInfoPage() {
|
||||
});
|
||||
toast.success("수정되었어요.");
|
||||
} else {
|
||||
// 신규 등록: allocateCode 호출하여 실제 순번 확보
|
||||
let finalItemNumber = formData.item_number || "";
|
||||
|
||||
if (numberingRuleIdRef.current) {
|
||||
try {
|
||||
const userInputCode = numberingTemplate.includes("____")
|
||||
? (() => {
|
||||
const parts = numberingTemplate.split("____");
|
||||
const prefix = parts[0] || "";
|
||||
const suffix = parts.slice(1).join("") || "";
|
||||
return prefix + manualInputValue + suffix;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
const allocRes = await apiClient.post(
|
||||
`/numbering-rules/${numberingRuleIdRef.current}/allocate`,
|
||||
{ formData, userInputCode }
|
||||
);
|
||||
|
||||
if (allocRes.data?.success && allocRes.data?.data?.generatedCode) {
|
||||
finalItemNumber = allocRes.data.data.generatedCode;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("채번 할당 실패:", err);
|
||||
toast.error("품목코드 할당에 실패했어요. 다시 시도해주세요.");
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { id, created_date, updated_date, ...insertFields } = formData;
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, { id: crypto.randomUUID(), ...insertFields });
|
||||
await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, {
|
||||
id: crypto.randomUUID(),
|
||||
...insertFields,
|
||||
item_number: finalItemNumber,
|
||||
});
|
||||
toast.success("등록되었어요.");
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
@@ -484,7 +610,15 @@ export default function ItemInfoPage() {
|
||||
/>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => {
|
||||
setIsModalOpen(open);
|
||||
if (!open) {
|
||||
setNumberingTemplate("");
|
||||
setManualInputValue("");
|
||||
setNumberingRule(null);
|
||||
numberingRuleIdRef.current = null;
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] w-[95vw] max-h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||
<DialogHeader className="shrink-0 px-6 pt-5 pb-3 border-b">
|
||||
<DialogTitle>{isEditMode ? "품목 수정" : "품목 등록"}</DialogTitle>
|
||||
@@ -534,6 +668,61 @@ export default function ItemInfoPage() {
|
||||
placeholder={field.label}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "numbering" ? (
|
||||
// 채번 세그먼트 UI
|
||||
isEditMode ? (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
) : isNumberingLoading ? (
|
||||
<div className="flex h-9 items-center gap-2 rounded-md border px-3">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">생성 중...</span>
|
||||
</div>
|
||||
) : numberingTemplate.includes("____") ? (
|
||||
(() => {
|
||||
const tplParts = numberingTemplate.split("____");
|
||||
const prefix = tplParts[0] || "";
|
||||
const suffix = tplParts.slice(1).join("") || "";
|
||||
return (
|
||||
<div className="flex h-9 items-center rounded-md border border-input">
|
||||
{prefix && (
|
||||
<span className="flex h-full items-center rounded-l-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={manualInputValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setManualInputValue(val);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
item_number: prefix + val + suffix,
|
||||
}));
|
||||
}}
|
||||
placeholder="입력"
|
||||
className="h-full min-w-[60px] flex-1 border-0 bg-transparent px-2 text-sm outline-none"
|
||||
/>
|
||||
{suffix && (
|
||||
<span className="flex h-full items-center rounded-r-[5px] bg-muted px-2 text-sm text-muted-foreground whitespace-nowrap">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
disabled
|
||||
placeholder="자동 채번"
|
||||
className="h-9 bg-muted"
|
||||
/>
|
||||
)
|
||||
) : ["selling_price", "standard_price"].includes(field.key) ? (
|
||||
<Input
|
||||
value={formData[field.key] ? Number(String(formData[field.key]).replace(/,/g, "")).toLocaleString() : ""}
|
||||
@@ -548,8 +737,7 @@ export default function ItemInfoPage() {
|
||||
<Input
|
||||
value={formData[field.key] || ""}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
||||
placeholder={field.placeholder || (field.disabled ? "자동 채번" : field.label)}
|
||||
disabled={field.disabled && !isEditMode}
|
||||
placeholder={field.placeholder || field.label}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -674,7 +674,10 @@ export default function ProductionPlanManagementPage() {
|
||||
manager_name: modalManager,
|
||||
work_order_no: modalWorkOrderNo,
|
||||
remarks: modalRemarks,
|
||||
equipment_id: modalEquipmentId ? Number(modalEquipmentId) : null,
|
||||
equipment_code: modalEquipmentId && modalEquipmentId !== "none" ? modalEquipmentId : null,
|
||||
equipment_name: modalEquipmentId && modalEquipmentId !== "none"
|
||||
? equipmentList.find((eq) => (eq.equipment_code || eq.id) === modalEquipmentId)?.equipment_name || null
|
||||
: null,
|
||||
} as any);
|
||||
if (res.success) {
|
||||
toast.success("생산계획이 수정되었습니다");
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function JeilGlassOrderPage() {
|
||||
const custs = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
optMap["partner_id"] = custs.map((c: any) => ({
|
||||
code: c.customer_code,
|
||||
label: `${c.customer_name} (${c.customer_code})`,
|
||||
label: c.customer_name,
|
||||
}));
|
||||
} catch { /* skip */ }
|
||||
// 담당자
|
||||
|
||||
Generated
+5
-37
@@ -270,7 +270,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -312,7 +311,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -346,7 +344,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -3062,7 +3059,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz",
|
||||
"integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/react-reconciler": "^0.32.0",
|
||||
@@ -3722,7 +3718,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz",
|
||||
"integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.6"
|
||||
},
|
||||
@@ -3817,7 +3812,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz",
|
||||
"integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -4161,7 +4155,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz",
|
||||
"integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
@@ -6662,7 +6655,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -6673,7 +6665,6 @@
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -6716,7 +6707,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
@@ -6799,7 +6789,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -7432,7 +7421,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8583,8 +8571,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3": {
|
||||
"version": "7.9.0",
|
||||
@@ -8906,7 +8893,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -9688,7 +9674,6 @@
|
||||
"integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9777,7 +9762,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -9879,7 +9863,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -11036,7 +11019,6 @@
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -11817,8 +11799,7 @@
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
@@ -13137,7 +13118,6 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -13431,7 +13411,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
@@ -13461,7 +13440,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
@@ -13510,7 +13488,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
|
||||
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
@@ -13714,7 +13691,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13784,7 +13760,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -13835,7 +13810,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
|
||||
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -13877,8 +13851,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "5.0.0",
|
||||
@@ -14186,7 +14159,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@@ -14209,8 +14181,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/recharts/node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@@ -15268,8 +15239,7 @@
|
||||
"version": "0.180.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
@@ -15357,7 +15327,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15706,7 +15675,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
Reference in New Issue
Block a user