Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2396f093b | ||
|
|
bdf65dcfd1 | ||
|
|
2aefbe17ee | ||
|
|
4482b92ccd | ||
|
|
d3c39caefb | ||
|
|
24c5dce24d | ||
|
|
d4d9eaf773 |
Vendored
+8
-118
@@ -14,91 +14,13 @@
|
||||
"dev:as"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 서버 컴포넌트 디버그 (node-terminal)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/as"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Playwright: 현재 활성화된 테스트 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"--filter",
|
||||
"cs",
|
||||
"exec",
|
||||
"playwright",
|
||||
"test",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Playwright: cs 로그인 테스트 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"--filter",
|
||||
"cs",
|
||||
"exec",
|
||||
"playwright",
|
||||
"test",
|
||||
"tests/login.spec.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:cs 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter cs dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/cs"
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -111,18 +33,13 @@
|
||||
"dev:cs"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -132,8 +49,7 @@
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
@@ -147,15 +63,7 @@
|
||||
"name": "dev:cs 클라이언트 디버그 (Chrome)",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/apps/cs",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/apps/cs/**",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
@@ -167,31 +75,13 @@
|
||||
"dev:st"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect --max-old-space-size=8192"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:st 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter st dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/st"
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Vendored
+1
-4
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pnpm": true
|
||||
},
|
||||
"playwright.configs": [
|
||||
"apps/cs/playwright.config.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
|
||||
const isSentryEnabled =
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENABLED === "true" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "dev" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "prd";
|
||||
|
||||
const getSentryHost = (dsn?: string) => {
|
||||
if (!dsn) return null;
|
||||
try {
|
||||
const url = new URL(dsn);
|
||||
return url.host;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const sentryHost = getSentryHost(process.env.NEXT_PUBLIC_SENTRY_DSN);
|
||||
|
||||
Sentry.init({
|
||||
enabled: isSentryEnabled,
|
||||
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({
|
||||
networkDetailAllowUrls: sentryHost
|
||||
? [
|
||||
new RegExp(
|
||||
`^(?!.*${sentryHost.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}).*(?:/api/|/server/).*$`,
|
||||
),
|
||||
]
|
||||
: [/\/(?:api|server)\//],
|
||||
networkRequestHeaders: ["X-Custom-Header"],
|
||||
networkResponseHeaders: ["X-Custom-Header"],
|
||||
}),
|
||||
],
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
+44
-25
@@ -1,34 +1,53 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
import path from "path";
|
||||
|
||||
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const webhwpCtrlBase = (
|
||||
process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl"
|
||||
).replace(/\/$/, "");
|
||||
|
||||
const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const;
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@common/assets"],
|
||||
outputFileTracingRoot: path.join(__dirname, "../../"),
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
experimental: {
|
||||
globalNotFound: true,
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
...webhwpAssetPrefixes.map((prefix) => ({
|
||||
source: `/webhwpctrl/${prefix}/:path*`,
|
||||
destination: `${webhwpCtrlBase}/${prefix}/:path*`,
|
||||
})),
|
||||
{
|
||||
source: "/webhwpctrl/file/:path*",
|
||||
destination: `${webhwpCtrlBase}/file/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const isSentryEnabled =
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENABLED === "true" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "dev" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "prd";
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
silent: true,
|
||||
widenClientFileUpload: true,
|
||||
tunnelRoute: "/monitoring",
|
||||
...(!isSentryEnabled ? { disable: true } : {}),
|
||||
webpack: {
|
||||
treeshake: {
|
||||
removeDebugLogging: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true" ? false : true,
|
||||
},
|
||||
automaticVercelMonitors: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
+3
-14
@@ -3,13 +3,11 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo -p 3003",
|
||||
"dev": "next dev -p 3003",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3003",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"lint:fsd": "steiger ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
@@ -23,13 +21,6 @@
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/context-zone": "^2.8.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.219.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.8.0",
|
||||
"@sentry/nextjs": "^10.57.0",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
@@ -55,8 +46,6 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.11",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "^4.1.10"
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const isSentryEnabled =
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENABLED === "true" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "dev" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "prd";
|
||||
|
||||
Sentry.init({
|
||||
enabled: isSentryEnabled,
|
||||
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",
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const isSentryEnabled =
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENABLED === "true" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "dev" &&
|
||||
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT !== "prd";
|
||||
|
||||
Sentry.init({
|
||||
enabled: isSentryEnabled,
|
||||
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",
|
||||
});
|
||||
@@ -76,7 +76,7 @@ function AprvrPickerDialog({ open, onOpenChange, atrzSeComCd, onSelect }: AprvrP
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="결재자 선택 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -162,7 +162,7 @@ function ElaprAplnPickerGrid({
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재선 선택 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -315,7 +315,7 @@ function ElaprApprovalDialog({
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재 진행 현황 목록으로 결재순서, 결재구분, 결재자, 전결자 지정, 결재상태, 관리 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="8%" />
|
||||
<col width="8%" />
|
||||
|
||||
@@ -66,6 +66,7 @@ function ElaprTmpltSelectTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 선택 목록",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -73,7 +73,7 @@ function AprvrPickerDialog({
|
||||
배치된 검토인력이 없습니다.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="검토인력 목록으로 성명, 부서, 직위, 직급, 선택 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
|
||||
@@ -185,7 +185,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
</p>
|
||||
) : (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<Table className="w-full">
|
||||
<Table caption="결재선 목록으로 결재순서, 결재구분, 결재자 정보 제공" className="w-full">
|
||||
<colgroup>
|
||||
<col width="5%" />
|
||||
<col width="10%" />
|
||||
|
||||
@@ -90,6 +90,7 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "결재서식 목록으로 결재서식명 정보 제공",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [row.elaprTmpltId]: true });
|
||||
onSelectTmplt(row);
|
||||
|
||||
@@ -67,7 +67,12 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {
|
||||
</div>
|
||||
),
|
||||
Empty: <NoData />,
|
||||
Success: () => <DataTable table={table} />,
|
||||
Success: () => (
|
||||
<DataTable
|
||||
table={table}
|
||||
caption="체크리스트 항목 목록: 항목명, 검토내용, 검토결과 정보 제공"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (values: CklstArtclFormValues) => {
|
||||
|
||||
@@ -83,6 +83,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "체크리스트 목록",
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [String(row.cklstSn)]: true });
|
||||
onSelectCklst(row);
|
||||
|
||||
@@ -51,7 +51,7 @@ function CklstPreviewDialog({
|
||||
{displayRows.length === 0 ? (
|
||||
<p className="py-6 text-center text-muted-foreground">표시할 항목이 없습니다.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<Table caption={`체크리스트 ${cklstNm} 미리보기`}>
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -277,7 +277,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
등록된 항목이 없습니다. [추가]로 1단계 항목을 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<Table caption="체크리스트 항목 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="36%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function AcntgCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="회계구분 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function CnstpCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="공종구분 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -103,7 +103,7 @@ function FnrsSharCodeTabPanel() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-[60rem] overflow-y-auto px-4 pb-4 pt-1">
|
||||
<Table className="w-full">
|
||||
<Table caption="재원분담 코드 목록" className="w-full">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="16%" />
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
}
|
||||
|
||||
.basinLabel.active {
|
||||
background: #006bf8;
|
||||
background: #0056b3;
|
||||
color: #fff;
|
||||
border-color: #96c3ff;
|
||||
box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3);
|
||||
|
||||
@@ -40,18 +40,18 @@ export function DashboardNotiList({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<section className="card card-border m-0 bg-[#FAFBFC]">
|
||||
<div className="card-header flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
알림이력
|
||||
{dataUpdatedAt > 0 ? (
|
||||
<span className="pl-2 text-lg font-medium text-muted-foreground">
|
||||
<span className="pl-2 text-lg font-medium text-[#4A4A4A]">
|
||||
(업데이트 일시: {dayjs(dataUpdatedAt).format("YYYY-MM-DD HH:mm")})
|
||||
</span>
|
||||
) : null}
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="button" size="sm" color="white" onClick={() => router.push("/nti-hstry")}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={() => router.push("/nti-hstry")}>
|
||||
전체보기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export function DashboardNotiList({
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{NOTI_STAT_CHIPS.map((chip) => (
|
||||
<Badge key={chip.field} color={chip.color}>
|
||||
<Badge key={chip.field} color={chip.color} className={chip.className}>
|
||||
{chip.label} {isPending ? "-" : (notiStats?.[chip.field] ?? 0)}건
|
||||
</Badge>
|
||||
))}
|
||||
@@ -75,7 +75,7 @@ export function DashboardNotiList({
|
||||
) : null}
|
||||
|
||||
{isError ? (
|
||||
<p className="flex min-h-[12rem] items-center justify-center text-muted-foreground">
|
||||
<p className="flex min-h-[12rem] items-center justify-center text-[#4A4A4A]">
|
||||
알림이력을 불러오지 못했습니다.
|
||||
</p>
|
||||
) : null}
|
||||
@@ -110,11 +110,11 @@ interface DashboardNotiRowProps {
|
||||
function DashboardNotiRow({ row, isActionPending, onAction }: DashboardNotiRowProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-4 border-b px-2 py-4 last:border-b-0">
|
||||
<span className="text-lg text-muted-foreground">
|
||||
<span className="text-lg text-[#4A4A4A]">
|
||||
{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}
|
||||
</span>
|
||||
<p className="min-w-0 line-clamp-2 text-xl sm:line-clamp-1">{row.notiTtl}</p>
|
||||
<Button type="button" size="sm" color="white" disabled={isActionPending} onClick={onAction}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" disabled={isActionPending} onClick={onAction}>
|
||||
처리하기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@ function resolveNotiUrl(row: NotiAsUserResponseDto) {
|
||||
}
|
||||
|
||||
const NOTI_STAT_CHIPS = [
|
||||
{ field: "todayCount", label: "오늘", color: "green" },
|
||||
{ field: "delayCount", label: "지연", color: "orange" },
|
||||
{ field: "pendingCount", label: "완료대기", color: "gray" },
|
||||
{ field: "todayCount", label: "오늘", color: "green", className: "!bg-[#188038]" },
|
||||
{ field: "delayCount", label: "지연", color: "orange", className: "!bg-[#C5221F]" },
|
||||
{ field: "pendingCount", label: "완료대기", color: "gray", className: "!bg-[#5F6368]" },
|
||||
] as const;
|
||||
|
||||
@@ -39,16 +39,17 @@ export function DashboardNoticeTable() {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "공지사항 목록",
|
||||
variant: "line",
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<section className="card card-border m-0 bg-[#FAFBFC]">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||
<Button type="button" size="sm" color="white" onClick={handleMoreClick}>
|
||||
<Button type="button" size="sm" color="white" className="!text-[#0056B3]" onClick={handleMoreClick}>
|
||||
더보기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function DashboardPage() {
|
||||
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex gap-4 px-[1.8rem] pb-6">
|
||||
<div className="flex min-h-full gap-4 bg-[#E9EDF2] px-[1.8rem] pb-6 pt-4 text-[#1E1E1E]">
|
||||
<div className="hidden flex-[0.5] sm:block">
|
||||
<NationalAsMap basinDocPrgrsList={data?.basinDocPrgrsList} />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-background text-muted-foreground">
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-[#FAFBFC] text-[#4A4A4A]">
|
||||
진행상황을 불러오지 못했습니다.
|
||||
</div>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
return (
|
||||
<div
|
||||
key={slot.docSeComCd}
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-background transition-colors"
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-[#FAFBFC] transition-colors"
|
||||
>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
||||
@@ -90,7 +90,7 @@ function DocPrgrsSttsMetric({
|
||||
>
|
||||
<Icon className="size-6" />
|
||||
</span>
|
||||
<span className="text-xl text-muted-foreground">{label}</span>
|
||||
<span className="text-xl text-[#4A4A4A]">{label}</span>
|
||||
<span className="text-3xl font-semibold tabular-nums">{count}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -107,21 +107,21 @@ const STAT_METRICS = [
|
||||
{
|
||||
field: "reqCnt",
|
||||
label: "신청",
|
||||
iconClassName: "bg-blue-500",
|
||||
iconClassName: "bg-[#4285F4]",
|
||||
bgColor: "bg-blue-100",
|
||||
Icon: FilePlus,
|
||||
},
|
||||
{
|
||||
field: "prgrsCnt",
|
||||
label: "진행",
|
||||
iconClassName: "bg-emerald-500",
|
||||
iconClassName: "bg-[#34A853]",
|
||||
bgColor: "bg-emerald-100",
|
||||
Icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
field: "cmptnCnt",
|
||||
label: "완료",
|
||||
iconClassName: "bg-orange-500",
|
||||
iconClassName: "bg-[#EA4335]",
|
||||
bgColor: "bg-orange-100",
|
||||
Icon: CircleCheck,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) {
|
||||
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
||||
|
||||
return (
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-background">
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-[#FAFBFC]">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
||||
</div>
|
||||
|
||||
@@ -165,8 +165,11 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.designCnstnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설계검토 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -45,7 +45,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -73,7 +73,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -117,7 +117,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="designCnstnMng.bizCn"
|
||||
@@ -137,9 +137,9 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 목록: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -180,10 +180,10 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
@@ -197,7 +197,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
@@ -234,7 +234,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -289,7 +289,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -100,6 +100,7 @@ function EsntlAtchFileList({
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "필수 첨부파일 관리 목록",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -115,6 +115,7 @@ function FileVrfcSplmntDialog({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "파일 검증 보완요청 목록",
|
||||
viewPagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ function PwrkCnsltnDocPjtcoAjmtTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업비 조정 내역: 구분, 검증결과, 검증금액 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="30%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -157,7 +157,7 @@ function CnsltnListTable({
|
||||
const isSomeSelected = selectedCount > 0 && !isAllSelected;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="자문 검토결과 목록: 구분, 검토의견 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="4%" />
|
||||
<col width="4%" />
|
||||
|
||||
@@ -248,8 +248,11 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.fnrsCnsltnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "재원협의 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -58,7 +58,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -86,7 +86,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -130,7 +130,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.bizCn"
|
||||
@@ -150,15 +150,15 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
{taskSeComCd.includes("RE") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>시설종류</TableHead>
|
||||
<TableHead scope="row">시설종류</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.rtlztnFcltSeComCd"
|
||||
@@ -198,7 +198,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>재이용용도</TableHead>
|
||||
<TableHead scope="row">재이용용도</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="fnrsCnsltnMng.rtlztnUsgSeComCd"
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -20,31 +20,31 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
<TableHead scope="row">사업명</TableHead>
|
||||
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||
<TableHead>사업기간</TableHead>
|
||||
<TableHead scope="row">사업기간</TableHead>
|
||||
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업내용</TableHead>
|
||||
<TableHead scope="row">사업내용</TableHead>
|
||||
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>환경청구분</TableHead>
|
||||
<TableHead scope="row">환경청구분</TableHead>
|
||||
<TableCell>{det.envoSeCdNm || "-"}</TableCell>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>신청지자체</TableHead>
|
||||
<TableHead scope="row">신청지자체</TableHead>
|
||||
<TableCell>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</TableCell>
|
||||
<TableHead>확정예산사업번호</TableHead>
|
||||
<TableHead scope="row">확정예산사업번호</TableHead>
|
||||
<TableCell>{det.cfmtnBgtBizNo || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>예산신청연도</TableHead>
|
||||
<TableHead scope="row">예산신청연도</TableHead>
|
||||
<TableCell colSpan={3}>{det.bgtAplyYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
@@ -51,9 +51,9 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
return (
|
||||
<TableRow key={node.acntgSeCd}>
|
||||
{node.treeLevel <= 1 ? (
|
||||
<TableHead className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
<TableHead scope="row" className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
) : (
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.acntgSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="재원협의 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -21,7 +21,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
||||
<SectionCollapsible title="재원분담">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 내역: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||
@@ -51,7 +51,7 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.fnrsSharSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.fnrsSharSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -189,7 +189,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -303,7 +303,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -1,891 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, simple-import-sort/imports */
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "@common/components";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
// InstlSncnApi imported below with types
|
||||
import { PROXY_HOST } from "@/shared/config";
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
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 { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn";
|
||||
import { client } from "@/shared/api";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
|
||||
const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
|
||||
const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
|
||||
ROLE_USER05: "LOCAL",
|
||||
ROLE_USER04: "LOCAL",
|
||||
ROLE_USER03: "ENV",
|
||||
ROLE_USER02: "KECO",
|
||||
ROLE_USER01: "KECO",
|
||||
};
|
||||
const REVIEW_STEP_ROLE_RANK: Record<ReviewStepCode, number> = {
|
||||
LOCAL: 1,
|
||||
ENV: 2,
|
||||
KECO: 3,
|
||||
};
|
||||
const INSTL_PROGRESS_ORDER = [
|
||||
"SV",
|
||||
"ST",
|
||||
"LE",
|
||||
"ER",
|
||||
"EE",
|
||||
"TE",
|
||||
"TS",
|
||||
"TR",
|
||||
"TP",
|
||||
"EP",
|
||||
"TA",
|
||||
"TB",
|
||||
];
|
||||
|
||||
const getProgressStepCount = (prgrsSttsComCd?: string): number => {
|
||||
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd ?? "ST");
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
|
||||
if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
|
||||
return 3;
|
||||
};
|
||||
|
||||
const getRoleStepCount = (dtlInstCd?: string): number => {
|
||||
const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined;
|
||||
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
|
||||
};
|
||||
|
||||
// 업무구분 코드 -> 표시명 매핑 (체크리스트 그룹 표시용)
|
||||
const TASK_CODE_LABELS: Record<string, string> = {
|
||||
RE: "재이용수",
|
||||
SP: "하수관로",
|
||||
SF: "하수처리시설",
|
||||
};
|
||||
|
||||
const normalizeChecklistResultMap = (
|
||||
raw: unknown,
|
||||
): Record<ReviewStepCode, any[]> => {
|
||||
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
|
||||
if (!raw) return base;
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
raw.forEach((row: any) => {
|
||||
const step = (row?.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
|
||||
if (REVIEW_STEP_CODES.includes(step)) {
|
||||
base[step].push(row);
|
||||
}
|
||||
});
|
||||
return base;
|
||||
}
|
||||
|
||||
if (typeof raw === "object") {
|
||||
REVIEW_STEP_CODES.forEach((step) => {
|
||||
const rows = (raw as Record<string, unknown>)[step];
|
||||
base[step] = Array.isArray(rows) ? (rows as any[]) : [];
|
||||
});
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
function CnfmViewPage({ id }: { id: string }) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-detail", id],
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
});
|
||||
|
||||
const data = (query.data ?? {}) as any;
|
||||
|
||||
// support API shape where payload includes detDto + lists
|
||||
const det = data.detDto ?? data;
|
||||
const bizSclList = data.bizSclList ?? [];
|
||||
const bizCstList = data.bizCstList ?? [];
|
||||
// API now provides a flat checklist result list: `cklstResultList`
|
||||
const rawCklst = useMemo(() => data.cklstResultList ?? [], [data.cklstResultList]);
|
||||
|
||||
// ensure we have a flat array
|
||||
const cklstAll = useMemo(() => (Array.isArray(rawCklst) ? (rawCklst as any[]) : []), [rawCklst]);
|
||||
|
||||
// build a step-keyed map from the flat list so the rest of the view can use same access pattern
|
||||
const cklstResultMap = useMemo(() => {
|
||||
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
|
||||
(cklstAll ?? []).forEach((row: any) => {
|
||||
const step = (row?.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
|
||||
if (REVIEW_STEP_CODES.includes(step)) base[step].push(row);
|
||||
});
|
||||
return base;
|
||||
}, [cklstAll]);
|
||||
const esntlFileList = data.esntlFileList ?? [];
|
||||
const etcFileList = data.etcFileList ?? [];
|
||||
const elaprDocPrgrsList = (data.elaprDocPrgrsList ?? []) as any[];
|
||||
const renderApproverCell = (code: string) => {
|
||||
const row = elaprDocPrgrsList.find((r) => r.atrzSeComCd === code);
|
||||
if (!row) return <div className="text-center">-</div>;
|
||||
const status = row.atrzPrgrsSttsComCd ?? "";
|
||||
const statusMap: Record<string, { label: string; badgeClass: string; icon: string }> = {
|
||||
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700", icon: "⏳" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800", icon: "📨" },
|
||||
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: "" };
|
||||
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}` : ""}` : ""}
|
||||
</div>
|
||||
<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={`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>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const orderedApprovers = useMemo(() => {
|
||||
return (elaprDocPrgrsList ?? []).slice().sort((a: any, b: any) => {
|
||||
const A = Number(a?.atrzSeqNo ?? 0);
|
||||
const B = Number(b?.atrzSeqNo ?? 0);
|
||||
return A - B;
|
||||
});
|
||||
}, [elaprDocPrgrsList]);
|
||||
|
||||
const handleApproveClick = async (elaprPrgrsId: any, atrzSeComCd: any) => {
|
||||
if (!elaprPrgrsId) {
|
||||
alert("결재 정보가 올바르지 않습니다.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await processElaprPrgrs(String(elaprPrgrsId), "ATRZ_PRGRS_003"); //진행상태를 승인으로 업데이트
|
||||
alert("결재가 완료되었습니다.");
|
||||
try {
|
||||
await query.refetch();
|
||||
} catch (e) {
|
||||
/* ignore refetch errors */
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("결재 처리 실패", err);
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재 처리 중 오류가 발생했습니다.";
|
||||
alert(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignApproverClick = async (elaprTmpltId: string) => {
|
||||
try {
|
||||
const payload = {
|
||||
elaprDocNo: det.instlSncnDocNo,
|
||||
elaprTmpltId: elaprTmpltId, /* 팝업에서 선택된 전자결제 템플릿 */
|
||||
dmndTtl: det.bizNm, /* "구례 하수도 처리시설 2026", */
|
||||
dmndCn: det.bizCn /*"구례 발주 하수도 처리시설 사업 2026년도 진행분 결제 요청 입니다."*/
|
||||
};
|
||||
// ElaprDmndApi 서비스를 통해 백엔드 연동
|
||||
await ElaprDmndApi.create(payload);
|
||||
alert("결재자 지정(결재요청 등록)이 완료되었습니다.");
|
||||
try {
|
||||
await query.refetch();
|
||||
} catch (e) {
|
||||
/* ignore refetch errors */
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("결재자 지정 실패", err);
|
||||
const msg = axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : "결재자 지정 중 오류가 발생했습니다.";
|
||||
alert(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const renderApproverRow = (row: any) => {
|
||||
if (!row) return <div className="text-center">-</div>;
|
||||
const status = row.atrzPrgrsSttsComCd ?? "";
|
||||
const statusMap: Record<string, { label: string; badgeClass: string; icon: string }> = {
|
||||
ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700", icon: "⏳" },
|
||||
ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800", icon: "📨" },
|
||||
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: "" };
|
||||
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}` : ""}` : ""}
|
||||
</div>
|
||||
<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={`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>
|
||||
</span>
|
||||
{status === "ATRZ_PRGRS_002" && userInfo?.userUnqId === row.aprvrUuid ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleApproveClick(row.elaprPrgrsId, row.atrzSeComCd)}
|
||||
className="ml-2 px-2 py-1 bg-blue-500 text-white text-xs rounded"
|
||||
>
|
||||
결제
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const currentPrgrsSttsComCd = (det.prgrsSttsComCd as string | undefined) ?? "ST";
|
||||
const roleBasedMaxStepCount = getRoleStepCount(userInfo?.dtlInstCd);
|
||||
const progressBasedMaxStepCount = getProgressStepCount(currentPrgrsSttsComCd);
|
||||
const visibleStepCount = Math.min(progressBasedMaxStepCount, roleBasedMaxStepCount);
|
||||
const visibleReviewStepCodes = useMemo(
|
||||
() => REVIEW_STEP_CODES.slice(0, visibleStepCount),
|
||||
[visibleStepCount],
|
||||
);
|
||||
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
|
||||
// selected task code for header filtering (null = none selected)
|
||||
const [selectedTaskCode, setSelectedTaskCode] = useState<string | null>(null);
|
||||
|
||||
// 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)));
|
||||
}, [cklstAll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleReviewStepCodes.length === 0) return;
|
||||
if (!visibleReviewStepCodes.includes(selectedReviewStep)) {
|
||||
setSelectedReviewStep(visibleReviewStepCodes[visibleReviewStepCodes.length - 1]);
|
||||
}
|
||||
}, [selectedReviewStep, visibleReviewStepCodes]);
|
||||
|
||||
// when checklist result task codes change, default selectedTaskCode to first available code
|
||||
useEffect(() => {
|
||||
if (!taskCodes || taskCodes.length === 0) {
|
||||
if (selectedTaskCode !== null) setSelectedTaskCode(null);
|
||||
return;
|
||||
}
|
||||
if (!selectedTaskCode || !taskCodes.includes(selectedTaskCode)) {
|
||||
setSelectedTaskCode(taskCodes[0]);
|
||||
}
|
||||
}, [taskCodes, selectedTaskCode]);
|
||||
|
||||
const selectedChecklistRows = useMemo(() => {
|
||||
if (visibleReviewStepCodes.length === 0) return [];
|
||||
const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) =>
|
||||
selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true,
|
||||
);
|
||||
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),
|
||||
);
|
||||
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length, selectedTaskCode]);
|
||||
|
||||
const [viewCheckResults, setViewCheckResults] = useState<Record<string, string>>({});
|
||||
const [viewCheckComments, setViewCheckComments] = useState<Record<string, 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 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) ?? "";
|
||||
return acc + (val ? 1 : 0);
|
||||
}, 0);
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
return { total, completed, percent };
|
||||
}, [cklstResultMap, viewCheckResults, selectedReviewStep, selectedTaskCode]);
|
||||
|
||||
const fnValidation = (mode: "SAVE" | "SUBMIT") => {
|
||||
// 기본 검증: 필요하면 확장
|
||||
return true;
|
||||
};
|
||||
|
||||
const hadleTechReviewSubmit = async () => {
|
||||
if (!fnValidation("SAVE")) return;
|
||||
|
||||
const cklstResultList = (cklstAll ?? [])
|
||||
.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;
|
||||
return {
|
||||
taskSeComCd: row.taskSeComCd ?? "",
|
||||
cklstSn: row.cklstSn,
|
||||
cklstArtclSn: row.cklstArtclSn,
|
||||
cklstRvwSn: row.cklstRvwSn,
|
||||
rvwStpComCd: selectedReviewStep,
|
||||
rvwRsltAnmlyYn,
|
||||
picRvwCn,
|
||||
};
|
||||
});
|
||||
|
||||
const payload: InstlSncnApplySaveRequestDto = {
|
||||
// only map core management DTO and checklist results; other lists empty per request
|
||||
instlSncnMng: { ...(det ?? {}), prgrsSttsComCd: "ER" } as any,
|
||||
bzScaleList: [],
|
||||
bizCstList: [],
|
||||
cklstResultList,
|
||||
esntlAtchFileList: [],
|
||||
} as unknown as InstlSncnApplySaveRequestDto;
|
||||
|
||||
try {
|
||||
const issuedDocNo = await InstlSncnApi.techReview(payload);
|
||||
alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`);
|
||||
} catch (error) {
|
||||
console.error("기술검토 등록 API 호출 실패", error);
|
||||
const serverMessage =
|
||||
axios.isAxiosError(error) && error.response?.data?.message
|
||||
? (error.response.data as { message: string }).message
|
||||
: "저장 중 오류가 발생했습니다.";
|
||||
alert(serverMessage);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const results: Record<string, 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.picRvwCn) comments[key] = String(row.picRvwCn);
|
||||
});
|
||||
setViewCheckResults(results);
|
||||
setViewCheckComments(comments);
|
||||
}, [selectedChecklistRows]);
|
||||
|
||||
if (query.isLoading) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 불러오는 중입니다...</div>;
|
||||
}
|
||||
|
||||
if (!query.data) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const fmt = (v: number | null | undefined) =>
|
||||
typeof v === "number" ? v.toLocaleString() : v === null || v === undefined ? "" : String(v);
|
||||
|
||||
const indentPx = (indNm: string | undefined) => {
|
||||
if (!indNm) return 0;
|
||||
const spaces = indNm.length - indNm.trimStart().length;
|
||||
return (spaces / 4) * 16;
|
||||
};
|
||||
|
||||
/* formatFileSize moved to shared lib: @/shared/lib/file-utils */
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
{/* orderedApprovers.length > 0 && ( */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">결재 정보</h3>
|
||||
<div className="cnfm-table-wrap mt-3">
|
||||
<table className="cnfm-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{orderedApprovers.length === 0 ? (
|
||||
<>
|
||||
<th className="text-center">접수자</th>
|
||||
<th className="text-center">검토자</th>
|
||||
<th className="text-center">승인자</th>
|
||||
<th className="text-center">최종승인자</th>
|
||||
</>
|
||||
) : (
|
||||
orderedApprovers.map((r: any) => (
|
||||
<th key={String(r.elaprPrgrsId ?? r.atrzSeComCd)} className="text-center">
|
||||
{r.atrzSeComNm ?? r.atrzSeComCd}
|
||||
</th>
|
||||
))
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{orderedApprovers.length === 0 ? (
|
||||
<>
|
||||
<td>{renderApproverCell("ATRZ01")}</td>
|
||||
<td>{renderApproverCell("ATRZ02")}</td>
|
||||
<td>{renderApproverCell("ATRZ03")}</td>
|
||||
<td>{renderApproverCell("ATRZ99")}</td>
|
||||
</>
|
||||
) : (
|
||||
orderedApprovers.map((r: any) => (
|
||||
<td key={String(r.elaprPrgrsId ?? r.atrzSeComCd)}>{renderApproverRow(r)}</td>
|
||||
))
|
||||
)}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/** 결제자 지정 버튼 */}
|
||||
<div className="mt-4 text-right">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAssignApproverClick("TMPLT20260611000001")}
|
||||
>
|
||||
결재자 지정
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* )} */}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">기본 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>사업명</th>
|
||||
<td>{det.bizNm || "-"}</td>
|
||||
<th>사업기간</th>
|
||||
<td>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업내용</th>
|
||||
<td colSpan={3}>{det.bizCn || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>구분</th>
|
||||
<td>{det.taskSeCdNm || det.taskSeComCd || "-"}</td>
|
||||
<th>신청지자체</th>
|
||||
<td>{det.sido || "-"} {det.gugun || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업구분</th>
|
||||
<td colSpan={3}>{det.sncnSeCdNm || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>진행상태</th>
|
||||
<td>{det.prgrsSttsCdNm || det.prgrsSttsComCd || "-"}</td>
|
||||
<th>예산사업코드</th>
|
||||
<td>{det.bgtBizCd || "-"}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">담당자 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>신청자명</th>
|
||||
<td>{det.aplcntNm || "-"}</td>
|
||||
<th>신청자 연락처</th>
|
||||
<td>{det.aplcntMblTelno || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>신청자 소속</th>
|
||||
<td>{det.aplcntOgdpNm || "-"}</td>
|
||||
<th>신청자 이메일</th>
|
||||
<td>{det.aplcntEmlAddr || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자명</th>
|
||||
<td>{det.dsgnrNm || "-"}</td>
|
||||
<th>설계자 연락처</th>
|
||||
<td>{det.dsgnrTelno || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자 소속</th>
|
||||
<td>{det.dsgnrOgdpNm || "-"}</td>
|
||||
<th>설계자 이메일</th>
|
||||
<td>{det.dsgnrEmlAddr || "-"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">이력 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>등록일시</th>
|
||||
<td>{det.regDt ? dayjs(det.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}</td>
|
||||
<th>수정일시</th>
|
||||
<td>{det.mdfcnDt ? dayjs(det.mdfcnDt).format("YYYY-MM-DD HH:mm:ss") : "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>등록자명</th>
|
||||
<td>{det.rgtrNm || "-"}</td>
|
||||
<th>첨부파일그룹ID</th>
|
||||
<td>{det.atchFileGroupId || "-"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 사업규모 */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">사업규모</h3>
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-[30rem]">구분</th>
|
||||
<th>기본계획(A)</th>
|
||||
<th>예산반영(B)</th>
|
||||
<th>금회설계시(C)</th>
|
||||
<th>증감(C-B)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bizSclList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="cnfm-td-empty">조회된 사업규모 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
bizSclList.map((node: any) => {
|
||||
const pl = indentPx(node.indCnstpSeCdNm);
|
||||
const bscPlanQty = Number(node.bscPlanQty) || 0;
|
||||
const budgetReflct = Number(node.bgtRfltQty) || 0;
|
||||
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>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 총사업비 */}
|
||||
<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>
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table w-full table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={2} className="w-[30rem]" rowSpan={2}>
|
||||
구분
|
||||
</th>
|
||||
<th rowSpan={2}>예산반영(A)</th>
|
||||
<th rowSpan={2}>표준사업비(B)</th>
|
||||
<th rowSpan={2}>금회설계시(C)</th>
|
||||
<th colSpan={2}>증감</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="w-[12rem]">(C-B)</th>
|
||||
<th className="w-[6rem]">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bizCstList.map((node: any) => {
|
||||
|
||||
const pl = indentPx(node.indAcntgSeCdNm);
|
||||
const bscPlanAmt = Number(node.bscPlanAmt) || 0;
|
||||
const budgetReflct = Number(node.bgtRfltAmt) || 0;
|
||||
const tstmDesignAmt = Number(node.tstmDesignAmt) || 0;
|
||||
const diff = tstmDesignAmt - budgetReflct;
|
||||
const showSubIcon = node.treeLevel >= 4;
|
||||
const isLeaf = !bizCstList.some((n: any) => n.upAcntgSeCd === node.acntgSeCd);
|
||||
return (
|
||||
<tr
|
||||
key={node.acntgSeCd}
|
||||
className={
|
||||
node.treeLevel === 1
|
||||
? "bg-blue-50 font-bold"
|
||||
: node.treeLevel === 2
|
||||
? "bg-gray-100 font-semibold"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{node.treeLevel <= 2 ? (
|
||||
<td colSpan={2} className="pl-2 text-left">{node.acntgSeCdNm}</td>
|
||||
) : (
|
||||
<>
|
||||
<td className="w-[4rem]"></td>
|
||||
<td
|
||||
className={`bg-gray-50 text-left ${isLeaf ? "text-gray-700" : ""}`}
|
||||
style={{ paddingLeft: `${pl + 8}px` }}
|
||||
>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.acntgSeCdNm}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
|
||||
<td>
|
||||
<div className="text-right">{fmt(bscPlanAmt)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{fmt(budgetReflct)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-right">{fmt(tstmDesignAmt)}</div>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 체크리스트 */}
|
||||
<div className="cnfm-card p-4">
|
||||
<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 gap-2 items-center">
|
||||
{Array.from(new Set(taskCodes)).map((code) => {
|
||||
const name = TASK_CODE_LABELS[code] ?? code;
|
||||
const active = selectedTaskCode === code;
|
||||
return (
|
||||
<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"
|
||||
}`}
|
||||
onClick={() => setSelectedTaskCode(code)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="text-[1.1rem] text-gray-500">{`(${cklstStats.completed}/${cklstStats.total})`}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{visibleReviewStepCodes.length === 0 ? (
|
||||
<span className="text-[1.2rem] text-gray-500">
|
||||
현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다.
|
||||
</span>
|
||||
) : (
|
||||
visibleReviewStepCodes.map((stepCode) => (
|
||||
<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"
|
||||
}`}
|
||||
onClick={() => setSelectedReviewStep(stepCode)}
|
||||
>
|
||||
{stepCode}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2 text-[1.25rem] text-gray-500">
|
||||
체크리스트 진행상황 [{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 rowSpan={2}>주요 검토내용</th>
|
||||
<th colSpan={2}>검토 결과(✓)</th>
|
||||
{selectedReviewStep !== "LOCAL" ? (
|
||||
<th rowSpan={2} className="w-[20rem]">기술검토 의견</th>
|
||||
) : null}
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="w-[9rem]">
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
예
|
||||
</button>
|
||||
</th>
|
||||
<th className="w-[9rem]">
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
아니오
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedChecklistRows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
selectedChecklistRows.map((row: any) => {
|
||||
const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`;
|
||||
const hasResult = row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N";
|
||||
const showRadios = row.bchckUseYn === "Y";
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</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>
|
||||
<td className="text-center">-</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="radio"
|
||||
name={`chk_${key}`}
|
||||
value="Y"
|
||||
checked={(viewCheckResults[key] ?? "") === "Y"}
|
||||
disabled={selectedReviewStep === "LOCAL"}
|
||||
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "Y" }))}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="radio"
|
||||
name={`chk_${key}`}
|
||||
value="N"
|
||||
checked={(viewCheckResults[key] ?? "") === "N"}
|
||||
disabled={selectedReviewStep === "LOCAL"}
|
||||
onChange={() => setViewCheckResults((prev) => ({ ...prev, [key]: "N" }))}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
{showRadios && selectedReviewStep !== "LOCAL" ? (
|
||||
<td className="text-left">
|
||||
<textarea
|
||||
className="cnfm-input w-full"
|
||||
value={viewCheckComments[key] ?? ""}
|
||||
onChange={(e) => setViewCheckComments((prev) => ({ ...prev, [key]: e.target.value }))}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필수 첨부파일 */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">필수 첨부파일</h3>
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>필수 자료명</th>
|
||||
<th>파일명</th>
|
||||
<th className="w-[8rem] text-right">크기</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="cnfm-td-empty">조회된 필수 첨부파일 항목이 없습니다.</td>
|
||||
</tr>
|
||||
) : (
|
||||
esntlFileList.map((f: any) => (
|
||||
<tr key={f.esntlAtchFileId}>
|
||||
<td className="pl-2">
|
||||
<div className="w-full text-left">
|
||||
{f.esntlAtchFileNm}
|
||||
{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>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="w-[8rem] text-right"><div className="text-right">{formatFileSize(f.atflFlieSz)}</div></td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기타 첨부파일 */}
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">기타 첨부파일</h3>
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>파일명</th>
|
||||
<th className="w-[8rem] text-right">크기</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<tr>
|
||||
<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/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>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-action-row">
|
||||
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
|
||||
<Button onClick={() => router.push(`/instl-sncn/${id}/modify`)}>수정</Button>
|
||||
<Button onClick={() => void hadleTechReviewSubmit()}>기술검토등록</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnfmViewPage };
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -209,8 +209,11 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto.instlSncnDocNo && (
|
||||
|
||||
@@ -59,6 +59,7 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "설치승인 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 기본 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -62,7 +62,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
사업명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -90,7 +90,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업기간</TableHead>
|
||||
<TableHead scope="row" data-required>사업기간</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldGroup className="flex-row items-center gap-2">
|
||||
<Controller
|
||||
@@ -134,7 +134,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업내용</TableHead>
|
||||
<TableHead scope="row" data-required>사업내용</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.bizCn"
|
||||
@@ -154,13 +154,13 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>구분</TableHead>
|
||||
<TableHead scope="row" data-required>구분</TableHead>
|
||||
<TableCell>{taskSeComCdLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>사업구분</TableHead>
|
||||
<TableHead scope="row" data-required>사업구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.sncnSeComCd"
|
||||
@@ -202,7 +202,7 @@ function BasicInfoFormSection() {
|
||||
{taskSeComCd.includes("RE") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>시설종류</TableHead>
|
||||
<TableHead scope="row">시설종류</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.rtlztnFcltSeComCd"
|
||||
@@ -242,7 +242,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>재이용용도</TableHead>
|
||||
<TableHead scope="row">재이용용도</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="instlSncnMng.rtlztnUsgSeComCd"
|
||||
|
||||
@@ -11,7 +11,7 @@ interface BasicInfoSectionProps {
|
||||
function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="기본 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 기본 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -20,25 +20,25 @@ function BasicInfoSection({ det }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>사업명</TableHead>
|
||||
<TableHead scope="row">사업명</TableHead>
|
||||
<TableCell>{det.bizNm || "-"}</TableCell>
|
||||
<TableHead>사업기간</TableHead>
|
||||
<TableHead scope="row">사업기간</TableHead>
|
||||
<TableCell>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업내용</TableHead>
|
||||
<TableHead scope="row">사업내용</TableHead>
|
||||
<TableCell colSpan={3}>{det.bizCn || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{det.taskSeCdNm || "-"}</TableCell>
|
||||
<TableHead>신청지자체</TableHead>
|
||||
<TableHead scope="row">신청지자체</TableHead>
|
||||
<TableCell>
|
||||
{det.sido || "-"} {det.gugun || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사업구분</TableHead>
|
||||
<TableHead scope="row">사업구분</TableHead>
|
||||
<TableCell colSpan={3}>{det.sncnSeCdNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -20,7 +20,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 내역: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]" rowSpan={2}>
|
||||
@@ -51,9 +51,9 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) {
|
||||
return (
|
||||
<TableRow key={node.acntgSeCd}>
|
||||
{node.treeLevel <= 1 ? (
|
||||
<TableHead className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
<TableHead scope="row" className="!px-[0.8rem] text-left">{node.acntgSeCdNm}</TableHead>
|
||||
) : (
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{node.acntgSeCdNm}
|
||||
</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 내역: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[30rem]">구분</TableHead>
|
||||
@@ -36,7 +36,7 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={node.cnstpSeCd}>
|
||||
<TableHead className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft: `${pl + 8}px` }}>
|
||||
{node.cnstpSeCdNm}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
|
||||
@@ -11,21 +11,21 @@ interface ContactInfoSectionProps {
|
||||
function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="설치승인 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{det.aplcntNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -33,16 +33,16 @@ function ContactInfoSection({ det }: ContactInfoSectionProps) {
|
||||
{det.dsgnrNm && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{det.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{det.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{det.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{det.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -180,10 +180,10 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
<TableHead scope="row" className="w-[12rem]">의견내용</TableHead>
|
||||
<TableCell>
|
||||
<Textarea
|
||||
value={formData.rvwRsltCn}
|
||||
@@ -197,7 +197,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검토결과</TableHead>
|
||||
<TableHead scope="row">검토결과</TableHead>
|
||||
<TableCell>
|
||||
<FieldSet>
|
||||
<RadioGroup
|
||||
@@ -234,7 +234,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableHead scope="row">첨부파일</TableHead>
|
||||
<TableCell>
|
||||
{uploadedFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -301,7 +301,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -70,6 +70,7 @@ function MyPbadmsSprtList({ searchParams, pagination, onPaginationChange }: MyPb
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "나의 행정지원 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@ function NtiHstryList({ searchParams, pagination, onPaginationChange }: NtiHstry
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "알림톡 발송 이력 목록",
|
||||
viewPagination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ function InPrgrsDocRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: InPrgrs
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "진행중 문서 담당자 수신자 목록" },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,6 +34,7 @@ function RvwMnpwRcvrList({ selectedRcvrKeys, onRcvrSelectionChange }: RvwMnpwRcv
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...rowSelectionOptions,
|
||||
},
|
||||
dataTableOptions: { caption: "검토인력 수신자 목록" },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,7 @@ function SndngList({
|
||||
|
||||
return (
|
||||
<div className="max-h-[40rem] overflow-auto rounded-md border">
|
||||
<Table variant="line" className="text-sm">
|
||||
<Table caption="알림톡 발송 대상 목록: 이름, 전화번호, 변수, 버튼 정보 제공" variant="line" className="text-sm">
|
||||
<TableHeader className="top-0">
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[100px]">이름</TableHead>
|
||||
|
||||
@@ -52,7 +52,7 @@ function ChecklistTaskPanel({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="체크리스트 검토 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
|
||||
@@ -35,7 +35,7 @@ function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 처리구역 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
@@ -86,7 +86,7 @@ function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const cells = getMetricCells(data, row.field);
|
||||
return (
|
||||
<TableRow key={row.field}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{cells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-right">
|
||||
{cell}
|
||||
|
||||
@@ -202,7 +202,7 @@ function PreviousPlanFileSection({ clsfSystmNm }: PreviousPlanFileSectionProps)
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 첨부파일 목록" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="!py-2 !text-xl">번호</TableHead>
|
||||
|
||||
@@ -13,7 +13,7 @@ function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="이전 기본계획 목표연도" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
@@ -54,7 +54,7 @@ function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>계획목표년도</TableHead>
|
||||
<TableHead scope="row">계획목표년도</TableHead>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-center">
|
||||
{cell}
|
||||
|
||||
@@ -57,7 +57,7 @@ function BasicInfoFormSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -66,7 +66,7 @@ function BasicInfoFormSection() {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
<TableHead scope="row" className="w-[20rem]" data-required>
|
||||
기본계획명
|
||||
</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
@@ -89,7 +89,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획년도</TableHead>
|
||||
<TableHead scope="row" data-required>계획년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.planYr"
|
||||
@@ -113,7 +113,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>기준년도</TableHead>
|
||||
<TableHead scope="row" data-required>기준년도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.crtrYr"
|
||||
@@ -139,7 +139,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>1단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>1단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.frlvlGoalYr"
|
||||
@@ -166,7 +166,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>2단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>2단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.sclvlGoalYr"
|
||||
@@ -192,7 +192,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>3단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>3단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.thlvlGoalYr"
|
||||
@@ -216,7 +216,7 @@ function BasicInfoFormSection() {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead data-required>4단계 목표연도</TableHead>
|
||||
<TableHead scope="row" data-required>4단계 목표연도</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.folvlGoalYr"
|
||||
@@ -242,7 +242,7 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획개요</TableHead>
|
||||
<TableHead scope="row" data-required>계획개요</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<Controller
|
||||
name="sewerBscPlanDtl.bizOtlnCn"
|
||||
@@ -264,9 +264,9 @@ function BasicInfoFormSection() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel || userInfo?.laeSeNm}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -18,7 +18,7 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="기본계획 신청">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 정보" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -27,37 +27,37 @@ function BasicInfoSection({ det, dtl }: BasicInfoSectionProps) {
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>기본계획명</TableHead>
|
||||
<TableHead scope="row">기본계획명</TableHead>
|
||||
<TableCell colSpan={3}>{det.sewerBscPlanNm || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>계획년도</TableHead>
|
||||
<TableHead scope="row">계획년도</TableHead>
|
||||
<TableCell>{dtl?.planYr || "-"}</TableCell>
|
||||
<TableHead>기준년도</TableHead>
|
||||
<TableHead scope="row">기준년도</TableHead>
|
||||
<TableCell>{dtl?.crtrYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>1단계 목표연도</TableHead>
|
||||
<TableHead scope="row">1단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.frlvlGoalYr || "-"}</TableCell>
|
||||
<TableHead>2단계 목표연도</TableHead>
|
||||
<TableHead scope="row">2단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.sclvlGoalYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>3단계 목표연도</TableHead>
|
||||
<TableHead scope="row">3단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.thlvlGoalYr || "-"}</TableCell>
|
||||
<TableHead>4단계 목표연도</TableHead>
|
||||
<TableHead scope="row">4단계 목표연도</TableHead>
|
||||
<TableCell>{dtl?.folvlGoalYr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>계획개요</TableHead>
|
||||
<TableHead scope="row">계획개요</TableHead>
|
||||
<TableCell colSpan={3} className="whitespace-pre-wrap">
|
||||
{dtl?.bizOtlnCn || "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead scope="row">구분</TableHead>
|
||||
<TableCell>{sewerBscPlanSeLabel || "-"}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableHead scope="row">신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -75,7 +75,7 @@ function BizCstMatrixFormSection() {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 총사업비 입력" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function BizCstMatrixFormSection() {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -62,7 +62,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||
<SectionCollapsible title="총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 총사업비 내역" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -89,7 +89,7 @@ function BizCstMatrixSection({ bizCstList, dtl }: BizCstMatrixSectionProps) {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -13,21 +13,21 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 담당자 정보" variant="secondary">
|
||||
<TableHeader>
|
||||
{dtl.aplcntNm ? (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>신청자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>신청자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{dtl.aplcntNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{dtl.aplcntMblTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{dtl.aplcntOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{dtl.aplcntEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
@@ -35,16 +35,16 @@ function ContactInfoSection({ dtl }: ContactInfoSectionProps) {
|
||||
{dtl.dsgnrNm ? (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>설계자</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row" rowSpan={2}>설계자</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>{dtl.dsgnrNm || "-"}</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>{dtl.dsgnrTelno || "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{dtl.dsgnrOgdpNm || "-"}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{dtl.dsgnrEmlAddr || "-"}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -85,7 +85,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="총사업비 대비 재원분담 합계 비교" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -107,7 +107,7 @@ function FnrsCstFormSection() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>총사업비</TableHead>
|
||||
<TableHead scope="row">총사업비</TableHead>
|
||||
{remainingList.map((item) => (
|
||||
<TableCell key={item.planStpNo}>
|
||||
<div className={cn("text-right", item.remaining !== 0 ? "text-destructive" : "")}>
|
||||
@@ -125,7 +125,7 @@ function FnrsCstFormSection() {
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 재원분담 입력" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -148,7 +148,7 @@ function FnrsCstFormSection() {
|
||||
<TableBody>
|
||||
{FNRS_CST_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STEPS.map((planStpNo) => {
|
||||
const item = fnrsCstList.find((cst) => cst.planStpNo === planStpNo);
|
||||
return (
|
||||
|
||||
@@ -79,7 +79,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="단계별 재원분담 내역" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function FnrsCstSection({ fnrsCstList, dtl }: FnrsCstSectionProps) {
|
||||
<TableBody>
|
||||
{FNRS_CST_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{stepHeaders.map((step) => {
|
||||
const item = matrixFnrsList.find((fnrs) => fnrs.planStpNo === step.planStpNo);
|
||||
return (
|
||||
|
||||
@@ -79,7 +79,7 @@ function OutlineFormSection() {
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업개요 단계별 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -102,7 +102,7 @@ function OutlineFormSection() {
|
||||
<TableBody>
|
||||
{OTLN_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{[1, 2, 3, 4].map((planStpNo) => {
|
||||
const item = otlnList.find((otln) => otln.planStpNo === planStpNo);
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업개요 단계별 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="22%" />
|
||||
@@ -52,7 +52,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
|
||||
<TableBody>
|
||||
{OTLN_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{stepHeaders.map((step) => {
|
||||
const item = otlnList.find((otln) => otln.planStpNo === step.planStpNo);
|
||||
return (
|
||||
|
||||
@@ -187,7 +187,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-semibold">검토의견 작성</h4>
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[12rem]">의견내용</TableHead>
|
||||
@@ -308,7 +308,7 @@ const TechReviewOpinionSection = forwardRef<
|
||||
function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="검토의견 목록: 의견자, 의견내용, 검토결과, 첨부파일, 등록일시 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="55%" />
|
||||
|
||||
@@ -147,7 +147,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리시설별 단계별 계획 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -199,7 +199,7 @@ function TmplntFacilityMatrix({
|
||||
<TableBody>
|
||||
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||
@@ -317,7 +317,7 @@ function TmplntFacilityAttachments({
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리구역 필수 첨부파일" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
@@ -332,7 +332,7 @@ function TmplntFacilityAttachments({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead data-required align="left">
|
||||
<TableHead scope="row" data-required align="left">
|
||||
{facilityName} 계획평면도
|
||||
</TableHead>
|
||||
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
|
||||
|
||||
@@ -102,7 +102,7 @@ function TmplntFacilityMatrix({
|
||||
const originalData = Array.isArray(data) ? (data[0] ?? null) : null;
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리시설별 단계별 계획 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="9.75%" />
|
||||
@@ -154,7 +154,7 @@ function TmplntFacilityMatrix({
|
||||
<TableBody>
|
||||
{TMPLNT_FIELD_ROWS.map((row) => (
|
||||
<TableRow key={row.key}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
<TableHead scope="row">{row.label}</TableHead>
|
||||
{PLAN_STP_NOS.map((planStpNo) => (
|
||||
<TableCell key={`original-${row.key}-${planStpNo}`} className="text-right">
|
||||
{getOriginalValue(originalData, row.key, planStpNo)}
|
||||
@@ -242,7 +242,7 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
|
||||
{(item.atchFileGroupId || item.atchFileInfo?.orgnlFileNm) && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table caption="처리구역 첨부파일" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="35%" />
|
||||
<col width="65%" />
|
||||
|
||||
@@ -117,7 +117,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<Table caption="기본계획 신청 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
@@ -127,7 +127,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
<TableHeader>
|
||||
{canSidoGugunSelectable && (
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableHead scope="row" data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
@@ -148,7 +148,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableHead scope="row">시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
@@ -170,7 +170,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>계획구분</TableHead>
|
||||
<TableHead scope="row" data-required>계획구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup
|
||||
@@ -199,7 +199,7 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableHead scope="row" data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup value={taskSeComCd || undefined} onValueChange={setTaskSeComCd}>
|
||||
|
||||
@@ -246,8 +246,11 @@ function SewerBscPlanFormPage({
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<div className="sticky top-[6rem] z-30 flex items-center justify-between bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="text-red-600">*</span>은 필수입력 항목입니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.sewerBscPlanDocNo && (
|
||||
|
||||
@@ -63,6 +63,7 @@ function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: Sewe
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "하수도 기본계획 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: handleRowClick,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import { matchQueryStatus } from "./match-query-status";
|
||||
interface DataTableOptions<T> {
|
||||
variant?: "default" | "secondary" | "line";
|
||||
className?: HTMLAttributes<HTMLTableElement>["className"];
|
||||
caption?: string;
|
||||
viewPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ function BnsPickerDialog({ open, onClose, onSelect }: BnsPickerDialogProps) {
|
||||
manualPagination: true,
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "사업 검색 결과 목록",
|
||||
viewPagination: true,
|
||||
onRowClick: (row) => {
|
||||
setRowSelection({ [getBnsListRowKey(row)]: true });
|
||||
@@ -330,7 +331,7 @@ function BnsPickerDialog({ open, onClose, onSelect }: BnsPickerDialogProps) {
|
||||
|
||||
function BnsBasicInfoTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
return (
|
||||
<Table variant="secondary" className="w-full">
|
||||
<Table caption="사업 검색 결과 목록" variant="secondary" className="w-full">
|
||||
<TableBody>
|
||||
<DetailPairRow
|
||||
leftLabel="시도"
|
||||
@@ -403,7 +404,7 @@ function BnsBasicInfoTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
|
||||
function BnsOperationTable({ detail }: { detail: BnsDetailResponseDto }) {
|
||||
return (
|
||||
<Table variant="secondary" className="w-full">
|
||||
<Table caption="선택 사업 상세 정보" variant="secondary" className="w-full">
|
||||
<TableBody>
|
||||
<DetailPairRow
|
||||
leftLabel="유입하수처리장"
|
||||
|
||||
@@ -380,7 +380,7 @@ function AttachmentsFormSection({
|
||||
{essentialFiles.length > 0 && (
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="필수 첨부파일 목록: 파일명, 파일 확장자, 파일 첨부 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="44%" />
|
||||
<col width="12%" />
|
||||
@@ -402,6 +402,7 @@ function AttachmentsFormSection({
|
||||
return (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableHead
|
||||
scope="row"
|
||||
className="group break-keep font-medium"
|
||||
data-required={file.esntlAtchYn === "Y"}
|
||||
align="left"
|
||||
|
||||
@@ -17,7 +17,7 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="첨부파일 목록" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
@@ -77,7 +77,7 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="첨부파일 목록" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
|
||||
@@ -58,7 +58,7 @@ function BizCstTotalDisplayTable({ costItems, sharItems }: BizCstTotalDisplayTab
|
||||
<h3 className="text-xl font-semibold">세부별 총사업비(①) - 재원분담 총사업비(②)</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary">
|
||||
<Table caption="세부별 총사업비와 재원분담 총사업비 비교" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
@@ -68,7 +68,7 @@ function BizCstTotalDisplayTable({ costItems, sharItems }: BizCstTotalDisplayTab
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>총사업비</TableHead>
|
||||
<TableHead scope="row">총사업비</TableHead>
|
||||
<TableCell>
|
||||
<div className={cn("text-right", hasExstnMismatch ? "text-destructive" : "")}>
|
||||
{formatRemaining(remaining.exstnRemaining)}
|
||||
|
||||
@@ -102,7 +102,7 @@ function BusinessCostFormSection() {
|
||||
<SectionCollapsible title="세부별 총사업비">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="세부별 총사업비 입력: 구분, 예산반영, 표준사업비, 금회설계, 증감 정보 제공" variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
@@ -140,7 +140,7 @@ function BusinessCostFormSection() {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -102,7 +102,7 @@ function BusinessScaleFormSection() {
|
||||
<SectionCollapsible title="사업규모">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="mb-2 text-right font-normal text-muted-foreground">(단위: 백만원)</p>
|
||||
<Table variant="secondary">
|
||||
<Table caption="사업규모 입력: 구분, 기본계획, 예산반영, 금회설계, 증감 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="20%" />
|
||||
@@ -130,7 +130,7 @@ function BusinessScaleFormSection() {
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
<div className="relative">
|
||||
{showSubIcon && (
|
||||
<span className="absolute -left-6 top-0 text-gray-400">└</span>
|
||||
|
||||
@@ -199,7 +199,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
{checklistTabs.map((tab) => {
|
||||
return (
|
||||
<TabsContent key={tab.code} value={tab.code} className="space-y-4">
|
||||
<Table variant="secondary">
|
||||
<Table caption={`${tab.label} 체크리스트 입력: 검토항목, 주요 검토내용, 검토결과 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="w-[30rem]">
|
||||
@@ -246,7 +246,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
|
||||
return (
|
||||
<TableRow key={item.cklstRvwSn}>
|
||||
<TableCell className="font-medium">{item.title}</TableCell>
|
||||
<TableHead scope="row" className="font-medium">{item.title}</TableHead>
|
||||
<TableCell
|
||||
style={{
|
||||
paddingLeft: `${Math.max(0, item.treeLevel - 1) * 16 + 8}px`,
|
||||
|
||||
@@ -23,28 +23,28 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="담당자 정보">
|
||||
<Table variant="secondary">
|
||||
<Table caption="담당자 정보 입력" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" rowSpan={2}>
|
||||
<TableHead scope="row" className="w-[20rem]" rowSpan={2}>
|
||||
신청자
|
||||
</TableHead>
|
||||
<TableHead className="w-[16rem]">이름</TableHead>
|
||||
<TableHead scope="row" className="w-[16rem]">이름</TableHead>
|
||||
<TableCell>{userInfo?.userName}</TableCell>
|
||||
<TableHead className="w-[16rem]">연락처</TableHead>
|
||||
<TableHead scope="row" className="w-[16rem]">연락처</TableHead>
|
||||
<TableCell>{userInfo?.mphn}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>{userInfo?.ogdpSeNm}</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>{userInfo?.email}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} data-required>
|
||||
<TableHead scope="row" rowSpan={2} data-required>
|
||||
설계자
|
||||
</TableHead>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead scope="row">이름</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={`${fieldPrefix}.dsgnrNm`}
|
||||
@@ -62,7 +62,7 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead>연락처</TableHead>
|
||||
<TableHead scope="row">연락처</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={`${fieldPrefix}.dsgnrTelno`}
|
||||
@@ -84,7 +84,7 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableHead scope="row">소속</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={`${fieldPrefix}.dsgnrOgdpNm`}
|
||||
@@ -102,7 +102,7 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableHead scope="row">이메일</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={`${fieldPrefix}.dsgnrEmlAddr`}
|
||||
|
||||
@@ -200,7 +200,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
||||
<h3 className="text-xl font-semibold">재원분담</h3>
|
||||
<span className="font-normal text-muted-foreground">(단위: 백만원)</span>
|
||||
</div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="재원분담 입력: 구분, 현행, 변경요구, 증감, 비고 정보 제공" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[24rem]" rowSpan={2}>
|
||||
@@ -234,7 +234,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
|
||||
|
||||
return (
|
||||
<TableRow key={item.code}>
|
||||
<TableHead className="text-left" style={{ paddingLeft }}>
|
||||
<TableHead scope="row" className="text-left" style={{ paddingLeft }}>
|
||||
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
|
||||
{item.label}
|
||||
</TableHead>
|
||||
|
||||
@@ -97,7 +97,7 @@ function PbadmsTaskApplyDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[66rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -105,17 +105,17 @@ function PbadmsTaskApplyDialog({
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<Table caption="행정지원 신청 정보 입력" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="20%" />
|
||||
<col width="30%" />
|
||||
<col width="20%" />
|
||||
<col width="30%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
{canSidoGugunSelectable && (
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableHead scope="row" data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
@@ -136,7 +136,7 @@ function PbadmsTaskApplyDialog({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableHead scope="row">시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
@@ -158,7 +158,7 @@ function PbadmsTaskApplyDialog({
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableHead scope="row" data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<FieldGroup data-slot="checkbox-group" className="flex-row flex-wrap">
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
} from "@common/components";
|
||||
|
||||
interface PrivacyConsentFormData {
|
||||
privacyConsent: "Y" | "N";
|
||||
@@ -23,7 +23,7 @@ function PrivacyConsentSection() {
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="개인정보 수집/이용 동의">
|
||||
<Table variant="secondary">
|
||||
<Table caption="개인정보 수집·이용 동의 내용" variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20rem]" data-required>
|
||||
|
||||
@@ -57,7 +57,7 @@ function ProgressStatusSection({
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="진행 상태">
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<Table caption="행정지원 진행상태" variant="secondary" className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
|
||||
@@ -239,7 +239,7 @@ function SplmntDmndDialog({
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="보완 요청 입력: 보완 항목, 보완 의견, 첨부파일 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="50%" />
|
||||
|
||||
@@ -240,7 +240,7 @@ function SplmntDmndFormSection({ initialData, readOnly = false }: SplmntDmndForm
|
||||
return (
|
||||
<SectionCollapsible title="보완요청">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="보완요청 및 처리 현황: 보완항목, 요청일시, 요청내용, 완료내용, 첨부파일, 완료여부 정보 제공" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="14%" />
|
||||
|
||||
@@ -28,7 +28,7 @@ function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProp
|
||||
return (
|
||||
<SectionCollapsible title="보완요청 및 완료 현황">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<Table caption="보완요청 현황" variant="secondary">
|
||||
<colgroup>
|
||||
<col width="14%" />
|
||||
<col width="8%" />
|
||||
|
||||
@@ -8,4 +8,5 @@ setCookie("site-id-key", SITE_ID);
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
redirectOnUnauthorized: false,
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user