Merge branch 'mhkim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node

This commit is contained in:
kjs
2026-04-28 09:21:19 +09:00
23 changed files with 1132 additions and 1656 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5936,65 +5936,12 @@ export class ScreenManagementService {
);
}
} else {
// 일반 사용자: 회사별 우선, 없으면 템플릿에서 자동 복제
// 일반 사용자: 회사별 레이아웃만 조회 (fallback/자동 복제 없음)
layout = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = $2`,
[screenId, companyCode],
);
// 회사별 레이아웃이 없으면 템플릿에서 자동 복제
if (!layout && companyCode !== "*") {
// 1. 공통(*) 템플릿 조회
let templateLayout = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = '*'`,
[screenId],
);
// 2. 공통 없으면 COMPANY_7(탑씰) 폴백
if (!templateLayout) {
templateLayout = await queryOne<{ layout_data: any }>(
`SELECT layout_data FROM screen_layouts_pop
WHERE screen_id = $1 AND company_code = 'COMPANY_7'`,
[screenId],
);
}
// 3. 템플릿이 있으면 해당 회사용으로 복제
if (templateLayout) {
console.log(`POP 레이아웃 자동 복제: screen_id=${screenId}, 대상 회사=${companyCode}`);
// 회사명 조회 (레이아웃 내 회사명 치환용)
const companyInfo = await queryOne<{ company_name: string }>(
`SELECT company_name FROM company_mng WHERE company_code = $1`,
[companyCode],
);
const companyName = companyInfo?.company_name || companyCode;
let clonedData = JSON.parse(JSON.stringify(templateLayout.layout_data));
// layout_data 내 회사명 텍스트 치환 (탑씰 관련 문자열 → 대상 회사명)
const layoutStr = JSON.stringify(clonedData);
const replacedStr = layoutStr
.replace(/\(주\)탑씰/g, companyName)
.replace(/탑씰/g, companyName)
.replace(/TOPSEAL/gi, companyName);
clonedData = JSON.parse(replacedStr);
// 해당 회사 코드로 INSERT (UPSERT)
await query(
`INSERT INTO screen_layouts_pop (screen_id, company_code, layout_data, created_at, updated_at, created_by, updated_by)
VALUES ($1, $2, $3, NOW(), NOW(), 'SYSTEM', 'SYSTEM')
ON CONFLICT (screen_id, company_code)
DO UPDATE SET layout_data = $3, updated_at = NOW(), updated_by = 'SYSTEM'`,
[screenId, companyCode, JSON.stringify(clonedData)],
);
console.log(`POP 레이아웃 자동 복제 완료: screen_id=${screenId}, company=${companyCode}`);
layout = { layout_data: clonedData };
}
}
}
if (!layout) {
@@ -6133,11 +6080,10 @@ export class ScreenManagementService {
[],
);
} else {
// 일반 회사: 해당 회사 레이아웃 + 공통(*)/COMPANY_7 템플릿도 포함
// (getLayoutPop에서 자동 복제하므로 템플릿이 있으면 해당 회사도 사용 가능)
// 일반 회사: 해당 회사 레이아웃만 조회 (자동 복제/fallback 없음)
result = await query<{ screen_id: number }>(
`SELECT DISTINCT screen_id FROM screen_layouts_pop
WHERE company_code IN ($1, '*', 'COMPANY_7')`,
WHERE company_code = $1`,
[companyCode],
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -831,7 +831,7 @@ export function WorkOrderList(props: WorkOrderListProps) {
const masterProcesses = useMemo(() => {
// 마스터 행 + 분할 행(진행중/완료/리워크) — 중복 제거
const seen = new Set<string>();
return allProcesses.filter((p) => {
const result = allProcesses.filter((p) => {
if (seen.has(p.id)) return false;
const include =
!p.parent_process_id || // 마스터 행
@@ -841,6 +841,32 @@ export function WorkOrderList(props: WorkOrderListProps) {
if (include) seen.add(p.id);
return include;
});
const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`;
const dist: Record<string, number> = {};
for (const p of result) {
const kind = p.parent_process_id ? "split" : "master";
const rw = isReworkProcess(p) ? "(R)" : "";
const k = `${p.process_code}:${kind}${rw}:${p.status}`;
dist[k] = (dist[k] || 0) + 1;
}
console.log(`${__tag} 3) masterProcesses 생성:`, {
input_allProcesses: allProcesses.length,
output_masterProcesses: result.length,
excluded: allProcesses.length - result.length,
distribution: dist,
});
console.log(`${__tag} 3-1) masterProcesses — P001 + 모든 리워크 상세:`);
console.table(result
.filter((p) => p.process_code === "P001" || isReworkProcess(p))
.map((p) => ({
id: p.id,
wo_id: p.wo_id,
process_code: p.process_code,
kind: p.parent_process_id ? "split" : "master",
status: p.status,
is_rework: p.is_rework,
})));
return result;
}, [allProcesses]);
const equipmentMap = useMemo(() => {
@@ -857,36 +883,56 @@ export function WorkOrderList(props: WorkOrderListProps) {
/* ---- Filtered processes ---- */
const filteredProcesses = useMemo(() => {
if (selectedProcess === "__all__") return []; // 공정 미선택 시 빈 목록
return masterProcesses.filter((proc) => {
const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`;
if (selectedProcess === "__all__") {
console.log(`${__tag} 4) filteredProcesses: 공정 미선택 — 빈 배열`);
return [];
}
const __excluded: Array<{ id: string; reason: string; status: string; process_code: string }> = [];
const result = masterProcesses.filter((proc) => {
const isRework = isReworkProcess(proc);
const isMaster = !proc.parent_process_id;
// 완료/진행중 탭에서는 SPLIT만 표시 (마스터 제외)
if (
isMaster &&
!isRework &&
(activeTab === "completed" || activeTab === "in_progress")
)
if (isMaster && !isRework && (activeTab === "completed" || activeTab === "in_progress")) {
__excluded.push({ id: proc.id, reason: "master_in_completed_or_in_progress_tab", status: proc.status, process_code: proc.process_code });
return false;
// 리워크 마스터가 in_progress/completed면 SPLIT이 생성된 것 → 리워크 마스터 숨김 (SPLIT은 표시)
if (
isRework &&
!proc.parent_process_id &&
(proc.status === "in_progress" || proc.status === "completed")
)
}
if (isRework && !proc.parent_process_id && (proc.status === "in_progress" || proc.status === "completed")) {
__excluded.push({ id: proc.id, reason: "rework_master_with_split", status: proc.status, process_code: proc.process_code });
return false;
// 재작업 카드는 공정 필터 무시 (모든 공정에서 표시)
if (!isRework && proc.process_code !== selectedProcess) return false;
}
if (!isRework && proc.process_code !== selectedProcess) {
__excluded.push({ id: proc.id, reason: `process_code_mismatch(${proc.process_code} vs ${selectedProcess})`, status: proc.status, process_code: proc.process_code });
return false;
}
if (selectedEquipment !== "__all__") {
const wi = instructionMap[proc.wo_id];
if (!wi) return false;
if (!wi) {
__excluded.push({ id: proc.id, reason: "no_wi_for_equipment_filter", status: proc.status, process_code: proc.process_code });
return false;
}
const eqId = wi.equipment_id;
const eq = equipmentMap[eqId];
if (!eq || eq.equipment_code !== selectedEquipment) return false;
if (!eq || eq.equipment_code !== selectedEquipment) {
__excluded.push({ id: proc.id, reason: `equipment_mismatch(${eq?.equipment_code} vs ${selectedEquipment})`, status: proc.status, process_code: proc.process_code });
return false;
}
}
if (activeTab !== "all" && proc.status !== activeTab) {
__excluded.push({ id: proc.id, reason: `tab_mismatch(${proc.status} vs ${activeTab})`, status: proc.status, process_code: proc.process_code });
return false;
}
if (activeTab !== "all" && proc.status !== activeTab) return false;
return true;
});
console.log(`${__tag} 4) filteredProcesses 계산:`, {
activeTab,
selectedProcess,
selectedEquipment,
input: masterProcesses.length,
output: result.length,
excluded_count: __excluded.length,
excluded_by_reason: __excluded.reduce((m: Record<string, number>, e) => { m[e.reason] = (m[e.reason] || 0) + 1; return m; }, {}),
});
return result;
}, [
masterProcesses,
selectedProcess,
@@ -923,23 +969,36 @@ export function WorkOrderList(props: WorkOrderListProps) {
waiting: 0,
completed: 0,
};
const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`;
const breakdown: Array<{ id: string; wo_id: string; process_code: string; status: string; kind: string; is_rework: string; bucket: string }> = [];
for (const proc of preFiltered) {
const isMaster = !proc.parent_process_id;
const isRw = isReworkProcess(proc);
// 리워크 마스터가 in_progress/completed면 SPLIT이 있으므로 카운트 제외
const kind = isMaster ? "master" : "split";
if (
isRw &&
!proc.parent_process_id &&
(proc.status === "in_progress" || proc.status === "completed")
)
) {
breakdown.push({ id: proc.id, wo_id: proc.wo_id, process_code: proc.process_code, status: proc.status, kind, is_rework: proc.is_rework || "", bucket: "★SKIPPED" });
continue;
if (proc.status === "acceptable") counts.acceptable++;
else if (proc.status === "in_progress" && (!isMaster || isRw))
counts.in_progress++;
else if (proc.status === "completed" && (!isMaster || isRw))
counts.completed++;
else counts.waiting++;
}
let bucket: string;
if (proc.status === "acceptable") { counts.acceptable++; bucket = "acceptable"; }
else if (proc.status === "in_progress" && (!isMaster || isRw)) { counts.in_progress++; bucket = "in_progress"; }
else if (proc.status === "completed" && (!isMaster || isRw)) { counts.completed++; bucket = "completed"; }
else { counts.waiting++; bucket = "waiting"; }
breakdown.push({ id: proc.id, wo_id: proc.wo_id, process_code: proc.process_code, status: proc.status, kind, is_rework: proc.is_rework || "", bucket });
}
console.log(`${__tag} 5) tabCounts 계산:`, {
selectedProcess,
selectedEquipment,
masterProcesses_total: masterProcesses.length,
preFiltered_total: preFiltered.length,
counts,
});
console.log(`${__tag} 5-1) 카드별 분류 breakdown (${breakdown.length}개):`);
console.table(breakdown);
return counts;
}, [
masterProcesses,

View File

@@ -77,25 +77,53 @@ export function useProcessData() {
if (inFlight.current) return inFlight.current;
const task = (async () => {
const __tag = `[POP-DEBUG][useProcessData][${new Date().toISOString()}]`;
console.log(`${__tag} ▶ fetchData 시작`, { withSync: !!opts?.withSync });
setLoading(true);
if (opts?.withSync) setSyncing(true);
try {
if (opts?.withSync) {
try {
await apiClient.post("/pop/production/sync-work-instructions");
} catch {
const __t0 = Date.now();
const syncRes = await apiClient.post("/pop/production/sync-work-instructions");
console.log(`${__tag} 0) POST /sync-work-instructions (${Date.now() - __t0}ms)`, syncRes.data);
} catch (e) {
console.warn(`${__tag} 0) sync 실패`, e);
toast.warning(
"동기화 실패 — 최신 작업지시가 반영되지 않을 수 있습니다",
);
}
}
const __t1 = Date.now();
const [wiRes, procRes, pmRes, eqRes] = await Promise.all([
apiClient.get("/work-instruction/list"),
apiClient.get("/pop/production/processes"),
dataApi.getTableData("process_mng", { size: 500 }),
dataApi.getTableData("equipment_mng", { size: 500 }),
]);
console.log(`${__tag} 1) 4개 API 병렬 호출 완료 (${Date.now() - __t1}ms)`);
const __wiArr = (Array.isArray(wiRes.data?.data) ? wiRes.data.data : wiRes.data?.data?.rows ?? []) as any[];
console.log(`${__tag} 1-1) /work-instruction/list 응답:`, {
raw_length: __wiArr.length,
unique_wi_ids: new Set(__wiArr.map((r: any) => r.wi_id || r.id)).size,
sample: __wiArr.slice(0, 3),
});
console.log(`${__tag} 1-2) /pop/production/processes 응답:`, {
raw_length: procRes.data?.data?.length ?? 0,
by_process_code: (procRes.data?.data ?? []).reduce((m: Record<string, number>, d: any) => { m[d.process_code] = (m[d.process_code] || 0) + 1; return m; }, {}),
});
console.log(`${__tag} 1-3) /pop/production/processes — P001 카드 RAW:`);
console.table((procRes.data?.data ?? [])
.filter((d: any) => d.process_code === "P001")
.map((d: any) => ({
id: d.id,
seq_no: d.seq_no,
parent: d.parent_process_id || "",
master_status: d.status,
wopr_count: d.accepted_results?.length ?? 0,
wopr_statuses: (d.accepted_results || []).map((r: any) => `${r.seq}:${r.status}${r.is_rework === "Y" ? "(R)" : ""}`).join("|"),
})));
// work-instruction: header+detail 조인이라 id 중복 → 첫 행만 취함
let wiRaw: Record<string, unknown>[] = [];
@@ -175,6 +203,55 @@ export function useProcessData() {
setAllProcesses(flat);
setProcessList((pmRes.data ?? []) as ProcessMng[]);
setEquipmentList((eqRes.data ?? []) as EquipmentMng[]);
// 2) flat 펼친 후 분포 — useProcessData가 master + 모든 wopr를 split row로 펼친 결과
const allDist: Record<string, number> = {};
const p001Dist: Record<string, number> = {};
const reworkDist: Record<string, number> = {};
for (const p of flat) {
const kind = p.parent_process_id ? "split" : "master";
const key = `${kind}:${p.status}`;
allDist[key] = (allDist[key] || 0) + 1;
if (p.process_code === "P001") {
p001Dist[key] = (p001Dist[key] || 0) + 1;
}
if (p.is_rework === "Y") {
reworkDist[`${p.process_code}:${kind}:${p.status}`] =
(reworkDist[`${p.process_code}:${kind}:${p.status}`] || 0) + 1;
}
}
console.log(`${__tag} 2) flat 펼침 완료:`, {
raw_wop_count: rawRows.length,
flat_count: flat.length,
expand_factor: flat.length / Math.max(1, rawRows.length),
all_kind_status_dist: allDist,
p001_kind_status_dist: p001Dist,
rework_dist: reworkDist,
});
console.log(`${__tag} 2-1) flat — P001 row 상세:`);
console.table(flat
.filter((p) => p.process_code === "P001")
.map((p) => ({
id: p.id,
wo_id: p.wo_id,
seq_no: p.seq_no,
kind: p.parent_process_id ? "split" : "master",
status: p.status,
is_rework: p.is_rework,
split_no: p.split_no ?? "",
})));
console.log(`${__tag} 2-2) flat — 모든 리워크 row:`);
console.table(flat
.filter((p) => p.is_rework === "Y")
.map((p) => ({
id: p.id,
wo_id: p.wo_id,
process_code: p.process_code,
seq_no: p.seq_no,
kind: p.parent_process_id ? "split" : "master",
status: p.status,
})));
console.log(`${__tag} ◀ fetchData 종료`);
} catch {
toast.error("데이터 조회 실패");
} finally {

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -143,10 +143,10 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
// POP 설정에서 배너 텍스트 로드 (POP화면설정에서 관리)
const { settings: popSettings } = usePopSettings();
const homeConfig = (popSettings as any)?.screens?.home;
const bannerEnabled = homeConfig?.bannerEnabled ?? true;
const bannerText = homeConfig?.bannerText;
const marqueeText = bannerText || "[공지] 금일 오후 3시 전체 안전교육 실시 예정입니다. 전 직원 필참 바랍니다. | [알림] 내일 설비 정기점검으로 인한 3호기 가동 중지 예정 | [안내] 4월 생산실적 우수팀 발표 - 생산1팀 축하드립니다!";
const mainConfig = (popSettings as any)?.screens?.main;
const bannerEnabled = mainConfig?.bannerEnabled ?? true;
const bannerText = mainConfig?.bannerText;
const marqueeText = bannerText || "";
return (
<div className="min-h-screen min-h-dvh flex flex-col" style={{ background: "#F5F5F5" }}>
@@ -402,7 +402,7 @@ export function PopShell({ children, showBanner = true, title, showBack = false,
)}
{/* ===== NOTICE BANNER (Marquee) ===== */}
{showBanner && bannerEnabled && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
{showBanner && bannerEnabled && bannerText && <div className="bg-amber-50 border-b border-amber-200 px-4 py-2 flex items-center gap-3">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-amber-600 text-sm">📢</span>
<span className="text-xs font-bold text-amber-700"></span>

View File

@@ -2,7 +2,6 @@
import { useRouter } from "next/navigation";
import type React from "react";
import { KpiCarousel, RecentActivity } from "@/components/pop/hardcoded";
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
interface MenuIconItem {
@@ -142,9 +141,7 @@ function LocalMenuIcons() {
export default function PopMainPage() {
return (
<>
<KpiCarousel />
<LocalMenuIcons />
<RecentActivity />
</>
);
}

View File

@@ -12,7 +12,7 @@ import { MessengerModal } from "@/components/messenger/MessengerModal";
export default function MainLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isPop = pathname.includes("/pop/") || pathname.endsWith("/pop");
const isPop = pathname.includes("/pop/");
if (isPop) {
return <>{children}</>;

View File

@@ -1,3 +1,5 @@
// DEPRECATED: 구 POP 라우트 그룹. 신 POP 는 frontend/app/(main)/COMPANY_X/pop/ 사용
import "@/app/globals.css";
export const metadata = {

View File

@@ -525,11 +525,27 @@ function AppLayoutInner({ children }: AppLayoutProps) {
// POP 모드 진입 핸들러
const handlePopModeClick = async () => {
if (isSuperAdmin) {
const userCompanyCode = (user as ExtendedUserInfo)?.companyCode;
// SUPER_ADMIN: 회사 미선택(*) 상태 → 회사 선택 모달
if (isSuperAdmin && userCompanyCode === "*") {
setPopCompanySelectOpen(true);
return;
}
// SUPER_ADMIN: 특정 회사 선택 상태 → 해당 회사 POP 으로 직행
if (isSuperAdmin && userCompanyCode && userCompanyCode !== "*") {
try {
if (!document.fullscreenElement) {
await document.documentElement.requestFullscreen();
}
} catch {
// 전체화면 미지원 또는 거부 시 무시
}
router.push(`/${userCompanyCode}/pop/main`);
return;
}
try {
// PC → POP 전환 시 전체화면 적용
try {
@@ -551,7 +567,6 @@ function AppLayoutInner({ children }: AppLayoutProps) {
} else if (childMenus.length === 1) {
router.push(childMenus[0].menu_url);
} else {
const userCompanyCode = (user as ExtendedUserInfo)?.companyCode;
if (userCompanyCode && userCompanyCode !== "*") {
router.push(`/${userCompanyCode}/pop/main`);
} else {

View File

@@ -1,3 +1,5 @@
// DEPRECATED: 구 POP 컴포넌트 묶음. 신 POP 는 frontend/app/(main)/COMPANY_X/pop/_components/ 사용
export {
InboundCart,
InboundTypeSelect,

View File

@@ -1,8 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { usePathname } from "next/navigation";
import { apiClient } from "@/lib/api/client";
import { useState } from "react";
export interface PopSettings {
version: string;
@@ -34,7 +32,7 @@ export interface PopSettings {
photoUpload: boolean;
barcodeEnabled: boolean;
};
home: {
main: {
kpiCarousel: boolean;
recentActivity: boolean;
bannerEnabled: boolean;
@@ -66,13 +64,13 @@ const DEFAULT_SETTINGS: PopSettings = {
version: "hardcoded-1.0",
screens: {
processExecution: {
materialInput: true,
photoUpload: true,
materialInput: false,
photoUpload: false,
plcEnabled: false,
bomFlexible: true,
packagingOptions: ["낱개", "박스", "파렛트"],
defectTypes: ["스크래치", "치수불량", "변색", "크랙", "기포"],
reworkTargetSelection: true,
bomFlexible: false,
packagingOptions: [],
defectTypes: [],
reworkTargetSelection: false,
groupPhotoEnabled: false,
dateFilter: false,
lastProcessInventory: "manual",
@@ -84,20 +82,20 @@ const DEFAULT_SETTINGS: PopSettings = {
inbound: {
inspectionRequired: false,
photoUpload: false,
barcodeEnabled: true,
barcodeEnabled: false,
packagingRecord: false,
defectSeparation: false,
},
outbound: {
photoUpload: false,
barcodeEnabled: true,
barcodeEnabled: false,
},
home: {
kpiCarousel: true,
recentActivity: true,
main: {
kpiCarousel: false,
recentActivity: false,
bannerEnabled: false,
bannerText: "",
iconThemeColor: "#2563eb",
iconThemeColor: "",
iconCustomImages: false,
dashboardLayout: "default",
},
@@ -110,142 +108,8 @@ const DEFAULT_SETTINGS: PopSettings = {
},
};
// URL -> screen_id mapping
const POP_SCREEN_MAP: Record<string, number> = {
"/pop/home": 6526,
"/pop/inbound": 6529,
"/pop/inbound/purchase": 6528,
"/pop/inbound/cart": 6527,
"/pop/outbound": 6,
"/pop/outbound/sales": 5,
"/pop/production": 8,
"/pop/production/process": 7,
"/COMPANY_7/pop/production/process": 7,
};
// URL -> settingsKey mapping
const PATH_TO_SETTINGS_KEY: Record<string, keyof PopSettings["screens"]> = {
"/pop/home": "home",
"/pop/inbound": "inbound",
"/pop/inbound/purchase": "inbound",
"/pop/inbound/cart": "inbound",
"/pop/outbound": "outbound",
"/pop/outbound/sales": "outbound",
"/pop/production": "processExecution",
"/pop/production/process": "processExecution",
"/COMPANY_7/pop/production/process": "processExecution",
};
// 신 URL `/COMPANY_X/pop/<tail>` 에서 화면 키 추출 (main → home 정규화)
function extractScreenKey(pathname: string): string | null {
const match = pathname.match(/^\/COMPANY_\d+\/pop\/(.+)$/);
if (!match) return null;
const tail = match[1];
return tail === "main" ? "home" : tail;
}
function getScreenIdFromPath(pathname: string): number | null {
// 신 URL 우선 처리 (회사 prefix 제거 후 화면 키 매핑)
const screenKey = extractScreenKey(pathname);
if (screenKey) {
const lookupPath = `/pop/${screenKey}`;
if (POP_SCREEN_MAP[lookupPath]) return POP_SCREEN_MAP[lookupPath];
const sortedNew = Object.keys(POP_SCREEN_MAP).sort((a, b) => b.length - a.length);
for (const path of sortedNew) {
if (lookupPath.startsWith(path)) return POP_SCREEN_MAP[path];
}
return null;
}
// 구 (pop)/ 라우트 호환 fallback
if (POP_SCREEN_MAP[pathname]) return POP_SCREEN_MAP[pathname];
const sorted = Object.keys(POP_SCREEN_MAP).sort((a, b) => b.length - a.length);
for (const path of sorted) {
if (pathname.startsWith(path)) return POP_SCREEN_MAP[path];
}
return null;
}
function getSettingsKeyFromPath(pathname: string): keyof PopSettings["screens"] | null {
// 신 URL 우선 처리 (회사 prefix 제거 후 화면 키 매핑)
const screenKey = extractScreenKey(pathname);
if (screenKey) {
const lookupPath = `/pop/${screenKey}`;
if (PATH_TO_SETTINGS_KEY[lookupPath]) return PATH_TO_SETTINGS_KEY[lookupPath];
const sortedNew = Object.keys(PATH_TO_SETTINGS_KEY).sort((a, b) => b.length - a.length);
for (const path of sortedNew) {
if (lookupPath.startsWith(path)) return PATH_TO_SETTINGS_KEY[path];
}
return null;
}
// 구 (pop)/ 라우트 호환 fallback
if (PATH_TO_SETTINGS_KEY[pathname]) return PATH_TO_SETTINGS_KEY[pathname];
const sorted = Object.keys(PATH_TO_SETTINGS_KEY).sort((a, b) => b.length - a.length);
for (const path of sorted) {
if (pathname.startsWith(path)) return PATH_TO_SETTINGS_KEY[path];
}
return null;
}
// Per-screenId cache to avoid redundant fetches
const screenCache: Record<number, Record<string, unknown>> = {};
export function usePopSettings(screenPath?: string) {
const autoPathname = usePathname();
const pathname = screenPath || autoPathname || "";
const [settings, setSettings] = useState<PopSettings>(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
const fetchSettings = useCallback(async () => {
const screenId = getScreenIdFromPath(pathname);
const settingsKey = getSettingsKeyFromPath(pathname);
if (!screenId || !settingsKey) {
setLoading(false);
return;
}
// Use cache if available
if (screenCache[screenId]) {
const popConfig = screenCache[screenId];
const merged = { ...DEFAULT_SETTINGS };
merged.screens = {
...merged.screens,
[settingsKey]: { ...merged.screens[settingsKey], ...popConfig },
};
setSettings(merged);
setLoading(false);
return;
}
try {
const res = await apiClient
.get(`/screen-management/screens/${screenId}/layout-pop`)
.catch(() => null);
if (res?.data?.data?.settings?.popConfig) {
const popConfig = res.data.data.settings.popConfig;
screenCache[screenId] = popConfig;
const merged = { ...DEFAULT_SETTINGS };
merged.screens = {
...merged.screens,
[settingsKey]: { ...merged.screens[settingsKey], ...popConfig },
};
setSettings(merged);
}
} catch {
// Use default settings on failure
}
setLoading(false);
}, [pathname]);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
export function usePopSettings(_screenPath?: string) {
const [settings] = useState<PopSettings>(DEFAULT_SETTINGS);
const [loading] = useState(false);
return { settings, loading };
}