fix: Improve numbering rule handling and item routing functionality

- Added temporary debug response in `numberingRuleController` for better troubleshooting.
- Refactored SQL queries in `NumberingRuleService` to enhance parameter handling and improve clarity.
- Updated `ItemInfoPage` to correctly handle manual input values for user-generated codes.
- Implemented sorting logic in `ItemRoutingTab` to prioritize default routing versions and added functionality to set a version as default.

These changes aim to enhance the reliability and user experience in managing numbering rules and item routing processes.
This commit is contained in:
kjs
2026-04-09 12:18:26 +09:00
parent 51eddc6d84
commit a9d2df48bf
16 changed files with 309 additions and 25 deletions

View File

@@ -323,6 +323,9 @@ router.post(
formData,
manualInputValue
);
// TODO: 디버그용 임시 응답 (나중에 제거)
const { getPool } = require("../database/db");
const dbPool = getPool();
return res.json({ success: true, data: { generatedCode: previewCode } });
} catch (error: any) {
logger.error("코드 미리보기 실패", { error: error.message });

View File

@@ -305,26 +305,26 @@ class NumberingRuleService {
if (hasCompanyCode && companyCode !== "*") {
sql = `
SELECT MAX(
CAST(SUBSTRING("${columnName}" FROM $1 FOR $2) AS INTEGER)
CAST(SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) AS INTEGER)
) as max_seq
FROM "${tableName}"
WHERE "${columnName}" LIKE $3
AND company_code = $4
AND LENGTH("${columnName}") = $5
AND SUBSTRING("${columnName}" FROM $1 FOR $2) ~ '^[0-9]+$'
WHERE "${columnName}" LIKE $1
AND company_code = $2
AND LENGTH("${columnName}") = $3
AND SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) ~ '^[0-9]+$'
`;
params = [seqStart, seqLength, likePattern, companyCode, prefixLen + seqLength + codeSuffix.length];
params = [likePattern, companyCode, prefixLen + seqLength + codeSuffix.length];
} else {
sql = `
SELECT MAX(
CAST(SUBSTRING("${columnName}" FROM $1 FOR $2) AS INTEGER)
CAST(SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) AS INTEGER)
) as max_seq
FROM "${tableName}"
WHERE "${columnName}" LIKE $3
AND LENGTH("${columnName}") = $4
AND SUBSTRING("${columnName}" FROM $1 FOR $2) ~ '^[0-9]+$'
WHERE "${columnName}" LIKE $1
AND LENGTH("${columnName}") = $2
AND SUBSTRING("${columnName}" FROM ${seqStart} FOR ${seqLength}) ~ '^[0-9]+$'
`;
params = [seqStart, seqLength, likePattern, prefixLen + seqLength + codeSuffix.length];
params = [likePattern, prefixLen + seqLength + codeSuffix.length];
}
const result = await client.query(sql, params);
@@ -1436,6 +1436,7 @@ class NumberingRuleService {
psInfo.prefix, psInfo.suffix, psInfo.seqLength, companyCode
);
if (maxFromTable > baseSeq) {
logger.info("미리보기: 테이블 내 최대값이 카운터보다 높음", {
ruleId, companyCode, currentSeq, maxFromTable,

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"

View File

@@ -556,8 +556,8 @@ export default function ItemInfoPage() {
if (numberingRuleIdRef.current) {
try {
const hasManual = numberingParts.some(p => p.isManual);
const userInputCode = hasManual
? buildCodeFromParts(numberingParts, manualInputValue)
const userInputCode = hasManual && manualInputValue
? manualInputValue
: undefined;
const allocRes = await apiClient.post(

View File

@@ -39,6 +39,7 @@ import {
type RoutingVersion,
} from "@/lib/api/processInfo";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
function normalizeDefaultFlag(v: RoutingVersion): boolean {
const raw = v.is_default as unknown;
@@ -210,6 +211,7 @@ export function ItemRoutingTab() {
return;
}
const list = res.data.map((v) => ({ ...v, is_default: normalizeDefaultFlag(v) }));
list.sort((a, b) => (a.is_default === b.is_default ? 0 : a.is_default ? -1 : 1));
setVersions(list);
const preferred = preferVersionId ? list.find((v) => v.id === preferVersionId) : undefined;
const def = list.find((v) => v.is_default);
@@ -399,6 +401,38 @@ export function ItemRoutingTab() {
}
};
// 선택 버전이 기본인지
const selectedVersionIsDefault = useMemo(() => {
if (!selectedVersionId) return false;
const v = versions.find((v) => v.id === selectedVersionId);
return v ? normalizeDefaultFlag(v) : false;
}, [selectedVersionId, versions]);
// 기본 라우팅으로 설정
const handleSetDefaultVersion = async () => {
if (!selectedVersionId || !selectedItem) return;
try {
// 기존 기본 해제
for (const v of versions) {
if (normalizeDefaultFlag(v) && v.id !== selectedVersionId) {
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: v.id },
updatedData: { is_default: false },
});
}
}
// 선택 버전 기본 설정
await apiClient.put(`/table-management/tables/item_routing_version/edit`, {
originalData: { id: selectedVersionId },
updatedData: { is_default: true },
});
toast.success("기본 라우팅으로 설정했어요");
await loadVersions(selectedItem, selectedVersionId);
} catch {
toast.error("기본 설정에 실패했어요");
}
};
const submitNewVersion = async () => {
if (!selectedItem) return;
const name = versionName.trim();
@@ -639,6 +673,12 @@ export function ItemRoutingTab() {
</Badge>
</div>
<div className="flex gap-2">
{selectedVersionId && !selectedVersionIsDefault && (
<Button variant="outline" size="sm" className="gap-1" onClick={handleSetDefaultVersion}>
<Star className="h-3.5 w-3.5 fill-warning text-warning" />
</Button>
)}
<Button
variant="secondary"
size="sm"