[as frontend] sentry 테스트 환경설정 추가
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -14,7 +14,7 @@ import axios from "axios";
|
||||
import { processElaprPrgrs } from "@/entities/elapr/api/elapr-prgrs-service";
|
||||
import { ElaprDmndApi } from "@/entities/elapr/api/elapr-dmnd-service";
|
||||
import { dayjs, useUserStore, useAlertStore } from "@/shared/lib";
|
||||
import { instlSncnService, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
|
||||
import { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
|
||||
import { client } from "@/shared/api";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
@@ -67,7 +67,9 @@ const TASK_CODE_LABELS: Record<string, string> = {
|
||||
SF: "하수처리시설",
|
||||
};
|
||||
|
||||
const normalizeChecklistResultMap = (raw: unknown): Record<ReviewStepCode, any[]> => {
|
||||
const normalizeChecklistResultMap = (
|
||||
raw: unknown,
|
||||
): Record<ReviewStepCode, any[]> => {
|
||||
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
|
||||
if (!raw) return base;
|
||||
|
||||
@@ -99,7 +101,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-detail", id],
|
||||
queryFn: () => instlSncnService.getById(id),
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
});
|
||||
|
||||
const data = (query.data ?? {}) as any;
|
||||
@@ -136,28 +138,20 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800", icon: "✔️" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800", icon: "✖️" },
|
||||
};
|
||||
const st = statusMap[status] ?? {
|
||||
label: status || "",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
icon: "",
|
||||
};
|
||||
const st = statusMap[status] ?? { label: status || "", badgeClass: "bg-gray-100 text-gray-700", icon: "" };
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="font-semibold">{row.aprvrUuidNm ?? "-"}</div>
|
||||
<div className="text-[0.95rem] text-gray-500">
|
||||
{row.ognzNm ? `${row.ognzNm}` : ""}
|
||||
{row.jbpsSeNm || row.jbgdSeNm
|
||||
? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}`
|
||||
: ""}
|
||||
{row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-center gap-2 text-[0.9rem]">
|
||||
{row.atrzDt ? (
|
||||
<span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span>
|
||||
) : null}
|
||||
<div className="text-[0.9rem] mt-1 flex items-center justify-center gap-2">
|
||||
{row.atrzDt ? <span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span> : null}
|
||||
{st.label ? (
|
||||
<span
|
||||
title={`${row.atrzPrgrsSttsComNm ?? st.label}${row.rjctRsn ? ` — 사유: ${row.rjctRsn}` : ""}${row.atrzDt ? ` · ${dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}` : ""}`}
|
||||
className={`ml-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold ${st.badgeClass}`}
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-semibold ml-1 ${st.badgeClass}`}
|
||||
>
|
||||
<span className="mr-1">{st.icon}</span>
|
||||
<span>{st.label}</span>
|
||||
@@ -190,10 +184,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("결재 처리 실패", err);
|
||||
const msg =
|
||||
axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: "결재 처리 중 오류가 발생했습니다.";
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재 처리 중 오류가 발생했습니다.";
|
||||
alert(msg);
|
||||
}
|
||||
};
|
||||
@@ -202,9 +193,9 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
try {
|
||||
const payload = {
|
||||
elaprDocNo: det.instlSncnDocNo,
|
||||
elaprTmpltId: elaprTmpltId /* 팝업에서 선택된 전자결제 템플릿 */,
|
||||
dmndTtl: det.bizNm /* "구례 하수도 처리시설 2026", */,
|
||||
dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/,
|
||||
elaprTmpltId: elaprTmpltId, /* 팝업에서 선택된 전자결제 템플릿 */
|
||||
dmndTtl: det.bizNm, /* "구례 하수도 처리시설 2026", */
|
||||
dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/
|
||||
};
|
||||
// ElaprDmndApi 서비스를 통해 백엔드 연동
|
||||
await ElaprDmndApi.create(payload);
|
||||
@@ -216,10 +207,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("결재자 지정 실패", err);
|
||||
const msg =
|
||||
axios.isAxiosError(err) && err.response?.data?.message
|
||||
? err.response.data.message
|
||||
: "결재자 지정 중 오류가 발생했습니다.";
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재자 지정 중 오류가 발생했습니다.";
|
||||
alert(msg);
|
||||
}
|
||||
};
|
||||
@@ -233,29 +221,21 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800", icon: "✔️" },
|
||||
ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800", icon: "✖️" },
|
||||
};
|
||||
const st = statusMap[status] ?? {
|
||||
label: status || "",
|
||||
badgeClass: "bg-gray-100 text-gray-700",
|
||||
icon: "",
|
||||
};
|
||||
const st = statusMap[status] ?? { label: status || "", badgeClass: "bg-gray-100 text-gray-700", icon: "" };
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="font-semibold">{row.aprvrUuidNm ?? "-"}</div>
|
||||
<div className="text-[0.95rem] text-gray-500">
|
||||
{row.ognzNm ? `${row.ognzNm}` : ""}
|
||||
{row.jbpsSeNm || row.jbgdSeNm
|
||||
? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}`
|
||||
: ""}
|
||||
{row.jbpsSeNm || row.jbgdSeNm ? ` · ${row.jbpsSeNm ?? ""}${row.jbgdSeNm ? ` ${row.jbgdSeNm}` : ""}` : ""}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-center gap-2 text-[0.9rem]">
|
||||
{row.atrzDt ? (
|
||||
<span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span>
|
||||
) : null}
|
||||
<div className="text-[0.9rem] mt-1 flex items-center justify-center gap-2">
|
||||
{row.atrzDt ? <span className="text-gray-400">{dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}</span> : null}
|
||||
{st.label ? (
|
||||
<>
|
||||
<span
|
||||
title={`${row.atrzPrgrsSttsComNm ?? st.label}${row.rjctRsn ? ` — 사유: ${row.rjctRsn}` : ""}${row.atrzDt ? ` · ${dayjs(row.atrzDt).format("YYYY-MM-DD HH:mm:ss")}` : ""}`}
|
||||
className={`ml-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold ${st.badgeClass}`}
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-semibold ml-1 ${st.badgeClass}`}
|
||||
>
|
||||
<span className="mr-1">{st.icon}</span>
|
||||
<span>{st.label}</span>
|
||||
@@ -264,7 +244,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleApproveClick(row.elaprPrgrsId, row.atrzSeComCd)}
|
||||
className="ml-2 rounded bg-blue-500 px-2 py-1 text-xs text-white"
|
||||
className="ml-2 px-2 py-1 bg-blue-500 text-white text-xs rounded"
|
||||
>
|
||||
결제
|
||||
</button>
|
||||
@@ -289,9 +269,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
|
||||
// derive available task codes from checklist results
|
||||
const taskCodes = useMemo(() => {
|
||||
return Array.from(
|
||||
new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean)),
|
||||
);
|
||||
return Array.from(new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean)));
|
||||
}, [cklstAll]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -319,11 +297,8 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
);
|
||||
return rows
|
||||
.slice()
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
(a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) ||
|
||||
(a.sortSeq ?? 0) - (b.sortSeq ?? 0) ||
|
||||
(a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0),
|
||||
.sort((a: any, b: any) =>
|
||||
(a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) || (a.sortSeq ?? 0) - (b.sortSeq ?? 0) || (a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0),
|
||||
);
|
||||
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length, selectedTaskCode]);
|
||||
|
||||
@@ -332,15 +307,11 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
|
||||
const cklstStats = useMemo(() => {
|
||||
if (!selectedReviewStep) return { total: 0, completed: 0, percent: 0 };
|
||||
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter(
|
||||
(r: any) =>
|
||||
r.bchckUseYn === "Y" &&
|
||||
(selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true),
|
||||
);
|
||||
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) => r.bchckUseYn === "Y" && (selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true));
|
||||
const total = rows.length;
|
||||
const completed = rows.reduce((acc: number, row: any) => {
|
||||
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
|
||||
const val = viewCheckResults[key] ?? row.rvwRsltAnmlyYn ?? "";
|
||||
const val = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? "";
|
||||
return acc + (val ? 1 : 0);
|
||||
}, 0);
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
@@ -359,8 +330,8 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
.filter((r: any) => r.bchckUseYn === "Y")
|
||||
.map((row: any) => {
|
||||
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`;
|
||||
const rvwRsltAnmlyYn = viewCheckResults[key] ?? row.rvwRsltAnmlyYn ?? null;
|
||||
const picRvwCn = viewCheckComments[key] ?? row.picRvwCn ?? null;
|
||||
const rvwRsltAnmlyYn = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? null;
|
||||
const picRvwCn = (viewCheckComments[key] ?? row.picRvwCn) ?? null;
|
||||
return {
|
||||
taskSeComCd: row.taskSeComCd ?? "",
|
||||
cklstSn: row.cklstSn,
|
||||
@@ -382,7 +353,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
} as unknown as InstlSncnApplySaveRequestDto;
|
||||
|
||||
try {
|
||||
const issuedDocNo = await instlSncnService.techReview(det.instlSncnDocNo, payload);
|
||||
const issuedDocNo = await InstlSncnApi.techReview(payload);
|
||||
alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`);
|
||||
} catch (error) {
|
||||
console.error("기술검토 등록 API 호출 실패", error);
|
||||
@@ -399,8 +370,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
const comments: Record<string, string> = {};
|
||||
(selectedChecklistRows ?? []).forEach((row: any) => {
|
||||
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
|
||||
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N")
|
||||
results[key] = row.rvwRsltAnmlyYn;
|
||||
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") results[key] = row.rvwRsltAnmlyYn;
|
||||
if (row.picRvwCn) comments[key] = String(row.picRvwCn);
|
||||
});
|
||||
setViewCheckResults(results);
|
||||
@@ -470,7 +440,10 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
</table>
|
||||
{/** 결제자 지정 버튼 */}
|
||||
<div className="mt-4 text-right">
|
||||
<Button type="button" onClick={() => handleAssignApproverClick("TMPLT20260611000001")}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAssignApproverClick("TMPLT20260611000001")}
|
||||
>
|
||||
결재자 지정
|
||||
</Button>
|
||||
</div>
|
||||
@@ -479,9 +452,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
{/* )} */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h2 className="cnfm-section-title">설치인가 상세보기</h2>
|
||||
<p className="mt-2 text-[1.3rem] text-gray-600">
|
||||
문서번호: <strong>{det.instlSncnDocNo}</strong>
|
||||
</p>
|
||||
<p className="mt-2 text-[1.3rem] text-gray-600">문서번호: <strong>{det.instlSncnDocNo}</strong></p>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
@@ -502,9 +473,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<th>구분</th>
|
||||
<td>{det.taskSeCdNm || det.taskSeComCd || "-"}</td>
|
||||
<th>신청지자체</th>
|
||||
<td>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</td>
|
||||
<td>{det.sido || "-"} {det.gugun || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업구분</th>
|
||||
@@ -516,6 +485,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<th>예산사업코드</th>
|
||||
<td>{det.bgtBizCd || "-"}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -589,9 +559,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<tbody>
|
||||
{bizSclList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="cnfm-td-empty">
|
||||
조회된 사업규모 항목이 없습니다.
|
||||
</td>
|
||||
<td colSpan={5} className="cnfm-td-empty">조회된 사업규모 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
bizSclList.map((node: any) => {
|
||||
@@ -601,28 +569,12 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
const tstmDesignQty = Number(node.tstmDesignQty) || 0;
|
||||
const diff = tstmDesignQty - budgetReflct;
|
||||
return (
|
||||
<tr
|
||||
key={node.cnstpSeCd}
|
||||
className={node.treeLevel === 1 ? "bg-gray-50 font-semibold" : ""}
|
||||
>
|
||||
<td
|
||||
className="bg-gray-50 pl-2 text-left"
|
||||
style={{ paddingLeft: `${pl + 8}px` }}
|
||||
>
|
||||
{node.cnstpSeCdNm}
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{fmt(bscPlanQty)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{fmt(budgetReflct)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{fmt(tstmDesignQty)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div>
|
||||
</td>
|
||||
<tr key={node.cnstpSeCd} className={node.treeLevel === 1 ? "bg-gray-50 font-semibold" : ""}>
|
||||
<td className="bg-gray-50 pl-2 text-left" style={{ paddingLeft: `${pl + 8}px` }}>{node.cnstpSeCdNm}</td>
|
||||
<td><div className="text-right">{fmt(bscPlanQty)}</div></td>
|
||||
<td><div className="text-right">{fmt(budgetReflct)}</div></td>
|
||||
<td><div className="text-right">{fmt(tstmDesignQty)}</div></td>
|
||||
<td><div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div></td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
@@ -634,9 +586,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
|
||||
{/* 총사업비 */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">
|
||||
총사업비 <span className="text-[1.2rem] font-normal text-gray-400">(단위: 백만원)</span>
|
||||
</h3>
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">총사업비 <span className="text-[1.2rem] font-normal text-gray-400">(단위: 백만원)</span></h3>
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table w-full table-fixed">
|
||||
<thead>
|
||||
@@ -656,6 +606,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{bizCstList.map((node: any) => {
|
||||
|
||||
const pl = indentPx(node.indAcntgSeCdNm);
|
||||
const bscPlanAmt = Number(node.bscPlanAmt) || 0;
|
||||
const budgetReflct = Number(node.bgtRfltAmt) || 0;
|
||||
@@ -675,9 +626,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
}
|
||||
>
|
||||
{node.treeLevel <= 2 ? (
|
||||
<td colSpan={2} className="pl-2 text-left">
|
||||
{node.acntgSeCdNm}
|
||||
</td>
|
||||
<td colSpan={2} className="pl-2 text-left">{node.acntgSeCdNm}</td>
|
||||
) : (
|
||||
<>
|
||||
<td className="w-[4rem]"></td>
|
||||
@@ -704,9 +653,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">
|
||||
{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}
|
||||
</div>
|
||||
<div className="text-right">{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -721,7 +668,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-[1.5rem] font-bold text-gray-700">체크리스트</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
{Array.from(new Set(taskCodes)).map((code) => {
|
||||
const name = TASK_CODE_LABELS[code] ?? code;
|
||||
const active = selectedTaskCode === code;
|
||||
@@ -729,11 +676,8 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<button
|
||||
key={code}
|
||||
type="button"
|
||||
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
|
||||
active
|
||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
||||
: "border-gray-300 bg-white text-gray-600"
|
||||
}`}
|
||||
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${active ? "border-blue-500 bg-blue-50 text-blue-700" : "border-gray-300 bg-white text-gray-600"
|
||||
}`}
|
||||
onClick={() => setSelectedTaskCode(code)}
|
||||
>
|
||||
{name}
|
||||
@@ -754,11 +698,10 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<button
|
||||
key={stepCode}
|
||||
type="button"
|
||||
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
|
||||
selectedReviewStep === stepCode
|
||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
||||
: "border-gray-300 bg-white text-gray-600"
|
||||
}`}
|
||||
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${selectedReviewStep === stepCode
|
||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
||||
: "border-gray-300 bg-white text-gray-600"
|
||||
}`}
|
||||
onClick={() => setSelectedReviewStep(stepCode)}
|
||||
>
|
||||
{stepCode}
|
||||
@@ -767,40 +710,40 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2 text-[1.25rem] text-gray-500">
|
||||
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> ·
|
||||
완료: <strong>{cklstStats.completed}</strong> · 진행률:{" "}
|
||||
<strong>{cklstStats.percent}%</strong>
|
||||
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> · 완료: <strong>{cklstStats.completed}</strong> · 진행률: <strong>{cklstStats.percent}%</strong>
|
||||
</div>
|
||||
<div className="cnfm-table-wrap mt-3">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-[20rem]" rowSpan={2}>
|
||||
검토항목
|
||||
</th>
|
||||
<th className="w-[20rem]" rowSpan={2}>검토항목</th>
|
||||
<th rowSpan={2}>주요 검토내용</th>
|
||||
<th colSpan={2}>검토 결과(✓)</th>
|
||||
{selectedReviewStep !== "LOCAL" ? (
|
||||
<th rowSpan={2} className="w-[20rem]">
|
||||
기술검토 의견
|
||||
</th>
|
||||
<th rowSpan={2} className="w-[20rem]">기술검토 의견</th>
|
||||
) : null}
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="w-[9rem]">
|
||||
<button type="button">예</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
예
|
||||
</button>
|
||||
</th>
|
||||
<th className="w-[9rem]">
|
||||
<button type="button">아니오</button>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
아니오
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedChecklistRows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="cnfm-td-empty">
|
||||
조회된 체크리스트 항목이 없습니다.
|
||||
</td>
|
||||
<td colSpan={5} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedChecklistRows.map((row: any) => {
|
||||
@@ -811,13 +754,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</td>
|
||||
<td
|
||||
className="pl-2 !text-left text-[1.2rem] text-gray-600"
|
||||
style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}
|
||||
>
|
||||
{row.treeLevel > 1 ? "└ " : ""}
|
||||
{row.rvwCn}
|
||||
</td>
|
||||
<td className="!text-left pl-2 text-[1.2rem] text-gray-600" style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}>{row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}</td>
|
||||
{!showRadios ? (
|
||||
<>
|
||||
<td className="text-center">-</td>
|
||||
@@ -832,9 +769,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
value="Y"
|
||||
checked={(viewCheckResults[key] ?? "") === "Y"}
|
||||
disabled={selectedReviewStep === "LOCAL"}
|
||||
onChange={() =>
|
||||
setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))
|
||||
}
|
||||
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
@@ -844,9 +779,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
value="N"
|
||||
checked={(viewCheckResults[key] ?? "") === "N"}
|
||||
disabled={selectedReviewStep === "LOCAL"}
|
||||
onChange={() =>
|
||||
setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))
|
||||
}
|
||||
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
@@ -856,9 +789,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<textarea
|
||||
className="cnfm-input w-full"
|
||||
value={viewCheckComments[key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))
|
||||
}
|
||||
onChange={(e) => setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
@@ -886,9 +817,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<tbody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="cnfm-td-empty">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</td>
|
||||
<td colSpan={3} className="cnfm-td-empty">조회된 필수 첨부파일 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
esntlFileList.map((f: any) => (
|
||||
@@ -896,28 +825,17 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<td className="pl-2">
|
||||
<div className="w-full text-left">
|
||||
{f.esntlAtchFileNm}
|
||||
{f.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
{f.esntlAtchYn === "Y" ? <span className="ml-1 text-red-500">*</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{f.atflCd ? (
|
||||
<a
|
||||
className="text-blue-600"
|
||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{f.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm || "다운로드"}</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="w-[8rem] text-right">
|
||||
<div className="text-right">{formatFileSize(f.atflFlieSz)}</div>
|
||||
</td>
|
||||
<td className="w-[8rem] text-right"><div className="text-right">{formatFileSize(f.atflFlieSz)}</div></td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -940,30 +858,19 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<tbody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={2} className="cnfm-td-empty">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</td>
|
||||
<td colSpan={2} className="cnfm-td-empty">등록된 기타첨부파일이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
etcFileList.map((f: any) => (
|
||||
<tr key={f.atflId}>
|
||||
<td>
|
||||
{f.orgnlFileNm ? (
|
||||
<a
|
||||
className="text-blue-600"
|
||||
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{f.orgnlFileNm}
|
||||
</a>
|
||||
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/download/${f.atflId}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
|
||||
) : (
|
||||
<span className="text-gray-700">{f.physFileNm || f.atflId}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="w-[8rem] text-right text-gray-500">
|
||||
{formatFileSize(f.atflFlieSz)}
|
||||
</td>
|
||||
<td className="w-[8rem] text-right text-gray-500">{formatFileSize(f.atflFlieSz)}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -973,9 +880,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
</div>
|
||||
|
||||
<div className="cnfm-action-row">
|
||||
<Button color="white" onClick={() => router.push("/instl-sncn")}>
|
||||
목록
|
||||
</Button>
|
||||
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
|
||||
<Button onClick={() => router.push(`/instl-sncn/${id}/modify`)}>수정</Button>
|
||||
<Button onClick={() => void hadleTechReviewSubmit()}>기술검토등록</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# apps/st/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3003
|
||||
NEXT_PUBLIC_SITE_ID=AS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3003
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://localhost:8000
|
||||
SERVER_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
|
||||
NEXT_PUBLIC_ST_SERVICE_URL=https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_ST_REALGRID_KEY=upVcPE+wPOmtLjqyBIh9RkM/nBOseBrflwxYpzGZyYm9cY8amGDkiHqyYT2U1Yh3Dufv8SUhNy63kJZj9DH/cmSe3PbBoyaMZTR5mNBsNuyY63NNzIfzqxzwYuagP76LkxValpjT3ck=
|
||||
|
||||
# 401 시 브라우저 리다이렉트 대상
|
||||
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
|
||||
|
||||
# Sentry Configuration
|
||||
NEXT_PUBLIC_SENTRY_DSN=http://f43f0aeff1cae8ea3f50a57f766ae37a@localhost:9001/2
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=local
|
||||
SENTRY_ORG=sentry
|
||||
SENTRY_PROJECT=as-frontend
|
||||
SENTRY_AUTH_TOKEN=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
|
||||
|
||||
# OpenTelemetry Tracing Configuration
|
||||
OTEL_SERVICE_NAME="as-frontend"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4320/v1/traces"
|
||||
|
||||
NEXT_PUBLIC_SENTRY_DEBUG=true
|
||||
Generated
+1020
-28
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user