This commit is contained in:
DDD1542
2026-04-10 13:34:01 +09:00
parent 06f9b54cd7
commit c8b1b1b742
4 changed files with 200 additions and 156 deletions
@@ -616,6 +616,7 @@ export default function PurchaseItemPage() {
page: 1, size: 500,
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
@@ -866,6 +867,7 @@ export default function PurchaseItemPage() {
{ columnName: "supplier_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
]}, autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
mappingRows = allMappings
@@ -887,7 +889,8 @@ export default function PurchaseItemPage() {
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
]}, autoFilter: true,
});
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
priceRows = allPriceData.map((p: any) => ({
_id: `p_existing_${p.id}`,
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
@@ -928,104 +931,104 @@ export default function PurchaseItemPage() {
const mappingRows = suppMappings[custKey] || [];
if (isEditingExisting && editSuppData?.id) {
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
const keptIds = new Set<string>();
let firstMappingId: string | null = null;
// 기존 매핑 조회
let existingMaps: any[] = [];
try {
const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
} catch { /* skip */ }
// 매핑 upsert: 인덱스 기반
const usedExistingIds = new Set<string>();
let firstMappingId: string | null = editSuppData.id;
for (let mi = 0; mi < mappingRows.length; mi++) {
const m = mappingRows[mi];
if (m._id?.startsWith("m_existing_")) {
const realId = m._id.replace("m_existing_", "");
keptIds.add(realId);
if (mi === 0) firstMappingId = realId;
const existMap = existingMaps[mi];
if (existMap) {
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
originalData: { id: realId },
originalData: { id: existMap.id },
updatedData: {
supplier_item_code: m.supplier_item_code || "",
supplier_item_name: m.supplier_item_name || "",
supplier_item_code: mappingRows[mi].supplier_item_code || "",
supplier_item_name: mappingRows[mi].supplier_item_name || "",
},
});
usedExistingIds.add(existMap.id);
if (mi === 0) firstMappingId = existMap.id;
} else {
const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
id: crypto.randomUUID(),
supplier_id: custKey, item_id: selectedItem.item_number,
supplier_item_code: m.supplier_item_code || "",
supplier_item_name: m.supplier_item_name || "",
supplier_item_code: mappingRows[mi].supplier_item_code || "",
supplier_item_name: mappingRows[mi].supplier_item_name || "",
});
if (mi === 0) firstMappingId = res.data?.data?.id || null;
if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null;
}
}
// 폼에서 삭제된 매핑 → DB 삭제
// 초과분 delete
const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id));
if (toDeleteMaps.length > 0) {
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
data: toDeleteMaps.map((m: any) => ({ id: m.id })),
});
}
// 기존 단가 조회
let existingPriceRows: any[] = [];
try {
const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
const existingPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
});
const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || [])
.filter((m: any) => !keptIds.has(m.id));
if (toDelete.length > 0) {
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
data: toDelete.map((m: any) => ({ id: m.id })),
});
}
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
} catch { /* skip */ }
if (!firstMappingId) firstMappingId = editSuppData.id;
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
const keptPriceIds = new Set<string>();
// 단가 upsert: 인덱스 기반
const priceRows = (suppPrices[custKey] || []).filter((p) =>
p.base_price || p.start_date || p.currency_code || p.base_price_type
);
for (const price of priceRows) {
if (price._id?.startsWith("p_existing_")) {
const realId = price._id.replace("p_existing_", "");
keptPriceIds.add(realId);
const usedPriceIds = new Set<string>();
for (let pi = 0; pi < priceRows.length; pi++) {
const price = priceRows[pi];
const priceData = {
mapping_id: firstMappingId || editSuppData.id,
supplier_id: custKey, item_id: selectedItem.item_number,
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
};
const existPrice = existingPriceRows[pi];
if (existPrice) {
await apiClient.put(`/table-management/tables/supplier_item_prices/edit`, {
originalData: { id: realId },
updatedData: {
mapping_id: firstMappingId || "",
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
},
originalData: { id: existPrice.id },
updatedData: priceData,
});
usedPriceIds.add(existPrice.id);
} else {
await apiClient.post(`/table-management/tables/supplier_item_prices/add`, {
id: crypto.randomUUID(),
mapping_id: firstMappingId || "",
supplier_id: custKey, item_id: selectedItem.item_number,
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
id: crypto.randomUUID(), ...priceData,
});
}
}
// 폼에서 삭제된 단가 → DB 삭제
try {
const dbPrices = await apiClient.post(`/table-management/tables/supplier_item_prices/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "supplier_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
// 초과분 delete
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id));
if (toDeletePrices.length > 0) {
await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, {
data: toDeletePrices.map((p: any) => ({ id: p.id })),
});
const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || [])
.filter((p: any) => !keptPriceIds.has(p.id));
if (toDeletePrices.length > 0) {
await apiClient.delete(`/table-management/tables/supplier_item_prices/delete`, {
data: toDeletePrices.map((p: any) => ({ id: p.id })),
});
}
} catch { /* skip */ }
}
} else {
// 신규 등록
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
@@ -1055,6 +1058,7 @@ export default function PurchaseItemPage() {
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
@@ -1719,7 +1723,7 @@ export default function PurchaseItemPage() {
{/* ── 공급업체 상세 입력/수정 모달 ── */}
<Dialog open={suppDetailOpen} onOpenChange={setSuppDetailOpen}>
<DialogContent className="max-w-[1100px] max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-[1100px] h-[85vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>
{editSuppData ? "수정" : "입력"} {selectedItem?.item_name || ""}
@@ -1729,7 +1733,7 @@ export default function PurchaseItemPage() {
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-2">
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
{selectedSuppsForDetail.map((cust, idx) => {
const custKey = cust.supplier_code || cust.id;
const mappingRows = suppMappings[custKey] || [];
@@ -264,6 +264,7 @@ export default function SalesItemPage() {
calculated_price: string;
}>>>({});
const [editCustData, setEditCustData] = useState<any>(null);
const [collapsedPriceCards, setCollapsedPriceCards] = useState<Set<string>>(new Set());
// 카테고리 로드
@@ -368,6 +369,7 @@ export default function SalesItemPage() {
page: 1, size: 500,
dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] },
autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
@@ -618,6 +620,7 @@ export default function SalesItemPage() {
{ columnName: "customer_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
]}, autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
const allMappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || [];
mappingRows = allMappings
@@ -639,7 +642,8 @@ export default function SalesItemPage() {
{ columnName: "item_id", operator: "equals", value: selectedItem!.item_number },
]}, autoFilter: true,
});
const allPriceData = priceRes.data?.data?.data || priceRes.data?.data?.rows || [];
const allPriceData = (priceRes.data?.data?.data || priceRes.data?.data?.rows || [])
.sort((a: any, b: any) => (a.start_date || "").localeCompare(b.start_date || ""));
priceRows = allPriceData.map((p: any) => ({
_id: `p_existing_${p.id}`,
start_date: p.start_date ? String(p.start_date).split("T")[0] : "",
@@ -680,104 +684,104 @@ export default function SalesItemPage() {
const mappingRows = custMappings[custKey] || [];
if (isEditingExisting && editCustData?.id) {
// 매핑: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
const keptIds = new Set<string>();
let firstMappingId: string | null = null;
// 기존 매핑 조회
let existingMaps: any[] = [];
try {
const existingMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "customer_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
sort: { columnName: "created_date", order: "asc" },
});
existingMaps = existingMappings.data?.data?.data || existingMappings.data?.data?.rows || [];
} catch { /* skip */ }
// 매핑 upsert: 인덱스 기반
const usedExistingIds = new Set<string>();
let firstMappingId: string | null = editCustData.id;
for (let mi = 0; mi < mappingRows.length; mi++) {
const m = mappingRows[mi];
if (m._id?.startsWith("m_existing_")) {
const realId = m._id.replace("m_existing_", "");
keptIds.add(realId);
if (mi === 0) firstMappingId = realId;
const existMap = existingMaps[mi];
if (existMap) {
await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, {
originalData: { id: realId },
originalData: { id: existMap.id },
updatedData: {
customer_item_code: m.customer_item_code || "",
customer_item_name: m.customer_item_name || "",
customer_item_code: mappingRows[mi].customer_item_code || "",
customer_item_name: mappingRows[mi].customer_item_name || "",
},
});
usedExistingIds.add(existMap.id);
if (mi === 0) firstMappingId = existMap.id;
} else {
const res = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
const mRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
id: crypto.randomUUID(),
customer_id: custKey, item_id: selectedItem.item_number,
customer_item_code: m.customer_item_code || "",
customer_item_name: m.customer_item_name || "",
customer_item_code: mappingRows[mi].customer_item_code || "",
customer_item_name: mappingRows[mi].customer_item_name || "",
});
if (mi === 0) firstMappingId = res.data?.data?.id || null;
if (mi === 0 && !firstMappingId) firstMappingId = mRes.data?.data?.id || null;
}
}
// 폼에서 삭제된 매핑 → DB 삭제
// 초과분 delete
const toDeleteMaps = existingMaps.filter((m) => !usedExistingIds.has(m.id));
if (toDeleteMaps.length > 0) {
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
data: toDeleteMaps.map((m: any) => ({ id: m.id })),
});
}
// 기존 단가 조회
let existingPriceRows: any[] = [];
try {
const dbMappings = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, {
const existingPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "customer_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
});
const toDelete = (dbMappings.data?.data?.data || dbMappings.data?.data?.rows || [])
.filter((m: any) => !keptIds.has(m.id));
if (toDelete.length > 0) {
await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, {
data: toDelete.map((m: any) => ({ id: m.id })),
});
}
existingPriceRows = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || [];
} catch { /* skip */ }
if (!firstMappingId) firstMappingId = editCustData.id;
// 단가: 기존→UPDATE, 신규→INSERT, 삭제된→DELETE
const keptPriceIds = new Set<string>();
// 단가 upsert: 인덱스 기반
const priceRows = (custPrices[custKey] || []).filter((p) =>
p.base_price || p.start_date || p.currency_code || p.base_price_type
);
for (const price of priceRows) {
if (price._id?.startsWith("p_existing_")) {
const realId = price._id.replace("p_existing_", "");
keptPriceIds.add(realId);
const usedPriceIds = new Set<string>();
for (let pi = 0; pi < priceRows.length; pi++) {
const price = priceRows[pi];
const priceData = {
mapping_id: firstMappingId || editCustData.id,
customer_id: custKey, item_id: selectedItem.item_number,
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
};
const existPrice = existingPriceRows[pi];
if (existPrice) {
await apiClient.put(`/table-management/tables/customer_item_prices/edit`, {
originalData: { id: realId },
updatedData: {
mapping_id: firstMappingId || "",
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
},
originalData: { id: existPrice.id },
updatedData: priceData,
});
usedPriceIds.add(existPrice.id);
} else {
await apiClient.post(`/table-management/tables/customer_item_prices/add`, {
id: crypto.randomUUID(),
mapping_id: firstMappingId || "",
customer_id: custKey, item_id: selectedItem.item_number,
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
id: crypto.randomUUID(), ...priceData,
});
}
}
// 폼에서 삭제된 단가 → DB 삭제
try {
const dbPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, {
page: 1, size: 100,
dataFilter: { enabled: true, filters: [
{ columnName: "customer_id", operator: "equals", value: custKey },
{ columnName: "item_id", operator: "equals", value: selectedItem.item_number },
]}, autoFilter: true,
// 초과분 delete
const toDeletePrices = existingPriceRows.filter((p) => !usedPriceIds.has(p.id));
if (toDeletePrices.length > 0) {
await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, {
data: toDeletePrices.map((p: any) => ({ id: p.id })),
});
const toDeletePrices = (dbPrices.data?.data?.data || dbPrices.data?.data?.rows || [])
.filter((p: any) => !keptPriceIds.has(p.id));
if (toDeletePrices.length > 0) {
await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, {
data: toDeletePrices.map((p: any) => ({ id: p.id })),
});
}
} catch { /* skip */ }
}
} else {
// 신규 등록
const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, {
@@ -807,6 +811,7 @@ export default function SalesItemPage() {
start_date: price.start_date || null, end_date: price.end_date || null,
currency_code: price.currency_code || null, base_price_type: price.base_price_type || null,
base_price: price.base_price ? Number(price.base_price) : null,
unit_price: price.calculated_price ? Number(price.calculated_price) : (price.base_price ? Number(price.base_price) : null),
discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null,
rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null,
calculated_price: price.calculated_price ? Number(price.calculated_price) : null,
@@ -823,7 +828,10 @@ export default function SalesItemPage() {
setSelectedItemId(null);
setTimeout(() => setSelectedItemId(sid), 50);
} catch (err: any) {
toast.error(err.response?.data?.message || "저장 실패했습니다.");
console.error("거래처 상세 저장 실패:", err.response?.data);
const detail = err.response?.data?.error?.details;
const msg = err.response?.data?.message || "저장에 실패했습니다.";
toast.error(detail ? `${msg} (${typeof detail === "string" ? detail : JSON.stringify(detail)})` : msg);
} finally {
setSaving(false);
}
@@ -1716,7 +1724,7 @@ export default function SalesItemPage() {
{/* ── 거래처 상세 입력/수정 모달 ── */}
<Dialog open={custDetailOpen} onOpenChange={setCustDetailOpen}>
<DialogContent className="max-w-[1100px] max-h-[90vh] overflow-y-auto">
<DialogContent className="max-w-[1100px] h-[85vh] flex flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>
{editCustData ? "수정" : "입력"} {selectedItem?.item_name || ""}
@@ -1726,7 +1734,7 @@ export default function SalesItemPage() {
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-2">
<div className="space-y-6 py-2 flex-1 overflow-y-auto">
{selectedCustsForDetail.map((cust, idx) => {
const custKey = cust.customer_code || cust.id;
const mappingRows = custMappings[custKey] || [];
@@ -1742,17 +1750,17 @@ export default function SalesItemPage() {
</span>
</div>
<div className="flex gap-4 p-4 bg-card">
<div className="flex gap-4 p-4 bg-card items-stretch">
{/* 좌: 거래처 품번/품명 */}
<div className="flex-1 border rounded-lg p-4 bg-muted/50">
<div className="flex items-center justify-between mb-3">
<div className="flex-1 border rounded-lg p-4 bg-muted/50 flex flex-col">
<div className="flex items-center justify-between mb-3 shrink-0">
<span className="text-sm font-semibold text-foreground"> / </span>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addMappingRow(custKey)}>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="space-y-2">
<div className="space-y-2 min-h-[200px] max-h-[350px] overflow-y-auto flex-1">
{mappingRows.length === 0 ? (
<div className="text-xs text-muted-foreground py-2"> </div>
) : (
@@ -1792,35 +1800,61 @@ export default function SalesItemPage() {
</div>
{/* 우: 기간별 단가 */}
<div className="flex-1 border rounded-lg p-4 bg-muted/30">
<div className="flex items-center justify-between mb-3">
<div className="flex-1 border rounded-lg p-4 bg-muted/30 flex flex-col">
<div className="flex items-center justify-between mb-3 shrink-0">
<span className="text-sm font-semibold text-foreground"> </span>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => addPriceRow(custKey)}>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="space-y-3">
<div className="space-y-3 flex-1 overflow-y-auto max-h-[350px]">
{prices.map((price, pIdx) => (
<div key={price._id} className="border rounded-lg p-3 bg-background space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground"> {pIdx + 1}</span>
<div key={price._id} className="border rounded-lg bg-background overflow-hidden">
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => setCollapsedPriceCards((prev) => {
const next = new Set(prev);
if (next.has(price._id)) next.delete(price._id); else next.add(price._id);
return next;
})}
>
<div className="flex items-center gap-2">
{collapsedPriceCards.has(price._id)
? <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
: <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
}
<span className="text-xs font-medium text-muted-foreground"> {pIdx + 1}</span>
{collapsedPriceCards.has(price._id) && price.calculated_price && (
<span className="text-xs text-muted-foreground ml-2">
{price.start_date || "—"} ~ {price.end_date || "—"} · <span className="font-mono font-bold text-foreground">{Number(price.calculated_price).toLocaleString()}</span> {priceCategoryOptions["currency_code"]?.find((o) => o.code === price.currency_code)?.label || ""}
</span>
)}
</div>
{prices.length > 1 && (
<Button
variant="ghost" size="sm"
className="h-6 w-6 p-0 text-destructive"
onClick={() => removePriceRow(custKey, price._id)}
onClick={(e) => { e.stopPropagation(); removePriceRow(custKey, price._id); }}
>
<X className="h-3 w-3" />
</Button>
)}
</div>
{!collapsedPriceCards.has(price._id) && <div className="px-3 pb-3 space-y-2">
{/* 기간 + 통화 */}
<div className="flex gap-2 items-center">
<Input
type="date"
value={price.start_date}
onChange={(e) => updatePriceRow(custKey, price._id, "start_date", e.target.value)}
onChange={(e) => {
const v = e.target.value;
updatePriceRow(custKey, price._id, "start_date", v);
if (price.end_date && v > price.end_date) {
updatePriceRow(custKey, price._id, "end_date", v);
}
}}
max={price.end_date || undefined}
className="h-8 text-xs flex-1"
/>
<span className="text-xs text-muted-foreground">~</span>
@@ -1828,6 +1862,7 @@ export default function SalesItemPage() {
type="date"
value={price.end_date}
onChange={(e) => updatePriceRow(custKey, price._id, "end_date", e.target.value)}
min={price.start_date || undefined}
className="h-8 text-xs flex-1"
/>
<div className="w-[80px]">
@@ -1922,6 +1957,7 @@ export default function SalesItemPage() {
{price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
</span>
</div>
</div>}
</div>
))}
</div>
+5 -1
View File
@@ -999,7 +999,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
</div>
{/* 테마 토글 */}
{(!isMobile && sidebarCollapsed) ? null : (
{(!isMobile && sidebarCollapsed) ? (
<div className="flex items-center justify-center border-t border-border py-2">
<ThemeToggle collapsed />
</div>
) : (
<div className="border-border border-t px-3 py-1">
<ThemeToggle />
</div>
+2 -2
View File
@@ -32,10 +32,10 @@ export function ThemeToggle({ collapsed = false }: ThemeToggleProps) {
variant="ghost"
size={collapsed ? "icon" : "default"}
onClick={() => setTheme(isDark ? "light" : "dark")}
className="w-full justify-start gap-2 text-sm"
className={collapsed ? "h-10 w-10 justify-center" : "w-full justify-start gap-2 text-sm"}
title={isDark ? "라이트 모드" : "다크 모드"}
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
{isDark ? <Sun className={collapsed ? "h-5 w-5" : "h-4 w-4"} /> : <Moon className={collapsed ? "h-5 w-5" : "h-4 w-4"} />}
{!collapsed && (isDark ? "라이트 모드" : "다크 모드")}
</Button>
);