diff --git a/.gitignore b/.gitignore index ee2cfd052..871b209f0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ settings.json .agent apps/**/tsconfig.tsbuildinfo +packages/**/tsconfig.tsbuildinfo logs/ diff --git a/apps/_template/tsconfig.json b/apps/_template/tsconfig.json index dab9b0b8e..e6c49fbe4 100644 --- a/apps/_template/tsconfig.json +++ b/apps/_template/tsconfig.json @@ -19,11 +19,15 @@ } ], "paths": { - "@/*": ["./src/*"], - "@common/components": ["../../packages/common/components/dist/"], - "@common/network": ["../../packages/common/network/"] + "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "../../packages/common/types/**/*.d.ts" + ], + "exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"] } diff --git a/apps/as/app/auth/login/page.tsx b/apps/as/app/(auth)/auth/login/page.tsx similarity index 100% rename from apps/as/app/auth/login/page.tsx rename to apps/as/app/(auth)/auth/login/page.tsx diff --git a/apps/as/app/(auth)/layout.tsx b/apps/as/app/(auth)/layout.tsx new file mode 100644 index 000000000..09d5b93ec --- /dev/null +++ b/apps/as/app/(auth)/layout.tsx @@ -0,0 +1,13 @@ +import "@/app/styles/globals.css"; + +export const metadata = { + title: "디지털 행정지원 시스템", +}; + +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/as/app/(with-base-layout)/approval-line-management/page.tsx b/apps/as/app/(main)/(with-base-layout)/approval-line-management/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/approval-line-management/page.tsx rename to apps/as/app/(main)/(with-base-layout)/approval-line-management/page.tsx diff --git a/apps/as/app/(with-base-layout)/checklist-management/page.tsx b/apps/as/app/(main)/(with-base-layout)/checklist-management/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/checklist-management/page.tsx rename to apps/as/app/(main)/(with-base-layout)/checklist-management/page.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/code-management/page.tsx b/apps/as/app/(main)/(with-base-layout)/code-management/page.tsx new file mode 100644 index 000000000..7a4daf43f --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/code-management/page.tsx @@ -0,0 +1 @@ +export { CodeManagementPage as default } from "@/pages/code-management"; diff --git a/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx b/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx new file mode 100644 index 000000000..1e20f5079 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/dashboard/page.tsx @@ -0,0 +1,3 @@ +import { DashboardPage } from "@/pages/dashboard/ui/dashboard-page"; + +export default DashboardPage; diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/edit/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/edit/page.tsx new file mode 100644 index 000000000..e5df52e22 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/edit/page.tsx @@ -0,0 +1,10 @@ +import { DesignEditPage } from "@/pages/design-cnstn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx new file mode 100644 index 000000000..f603f8e39 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/[id]/page.tsx @@ -0,0 +1,10 @@ +import { DesignDetailGate } from "@/pages/design-cnstn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/page.tsx new file mode 100644 index 000000000..cced4a877 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/page.tsx @@ -0,0 +1,3 @@ +import { DesignListPage } from "@/pages/design-cnstn"; + +export default DesignListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/design-cnstn/regist/page.tsx b/apps/as/app/(main)/(with-base-layout)/design-cnstn/regist/page.tsx new file mode 100644 index 000000000..4b57da09a --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/design-cnstn/regist/page.tsx @@ -0,0 +1,27 @@ +import { DesignRegistPage } from "@/pages/design-cnstn"; +import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema"; + +interface PageProps { + searchParams: Promise<{ + taskSeComCd?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }>; +} + +export default async function Page({ searchParams }: PageProps) { + const params = await searchParams; + const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd); + + return ( + + ); +} diff --git a/apps/as/app/(with-base-layout)/esntl-atch-file-mng/page.tsx b/apps/as/app/(main)/(with-base-layout)/esntl-atch-file-mng/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/esntl-atch-file-mng/page.tsx rename to apps/as/app/(main)/(with-base-layout)/esntl-atch-file-mng/page.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx new file mode 100644 index 000000000..f1371598e --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/edit/page.tsx @@ -0,0 +1,10 @@ +import { FnrsEditPage } from "@/pages/fnrs-cnsltn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx new file mode 100644 index 000000000..0d0df0607 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/[id]/page.tsx @@ -0,0 +1,10 @@ +import { FnrsDetailGate } from "@/pages/fnrs-cnsltn"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +export default async function Page({ params }: PageProps) { + const { id } = await params; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/page.tsx new file mode 100644 index 000000000..734a0afd9 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/page.tsx @@ -0,0 +1,3 @@ +import { FnrsListPage } from "@/pages/fnrs-cnsltn"; + +export default FnrsListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/regist/page.tsx b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/regist/page.tsx new file mode 100644 index 000000000..6af4150ed --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/fnrs-cnsltn/regist/page.tsx @@ -0,0 +1,27 @@ +import { FnrsRegistPage } from "@/pages/fnrs-cnsltn"; +import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema"; + +interface PageProps { + searchParams: Promise<{ + taskSeComCd?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }>; +} + +export default async function Page({ searchParams }: PageProps) { + const params = await searchParams; + const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd); + + return ( + + ); +} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/[id]/edit/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/edit/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/instl-sncn/[id]/edit/page.tsx rename to apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/edit/page.tsx diff --git a/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx similarity index 56% rename from apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx rename to apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx index 89cd7a329..f89257bcc 100644 --- a/apps/as/app/(with-base-layout)/instl-sncn/[id]/page.tsx +++ b/apps/as/app/(main)/(with-base-layout)/instl-sncn/[id]/page.tsx @@ -1,6 +1,6 @@ -import { InstlViewPage } from "@/pages/instl-sncn"; +import { InstlSncnDetailGate } from "@/pages/instl-sncn"; export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - return ; + return ; } diff --git a/apps/as/app/(with-base-layout)/instl-sncn/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/instl-sncn/page.tsx rename to apps/as/app/(main)/(with-base-layout)/instl-sncn/page.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/instl-sncn/regist/page.tsx b/apps/as/app/(main)/(with-base-layout)/instl-sncn/regist/page.tsx new file mode 100644 index 000000000..ff047fd96 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/instl-sncn/regist/page.tsx @@ -0,0 +1,28 @@ +import { InstlRegistPage } from "@/pages/instl-sncn"; + +interface InstlSncnRegistRouteProps { + searchParams: Promise<{ + taskSeComCd?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }>; +} + +export default async function Page({ searchParams }: InstlSncnRegistRouteProps) { + const params = await searchParams; + const taskSeComCd = params.taskSeComCd + ? params.taskSeComCd.split(",").filter(Boolean) + : undefined; + + return ( + + ); +} diff --git a/apps/as/app/(with-base-layout)/layout.tsx b/apps/as/app/(main)/(with-base-layout)/layout.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/layout.tsx rename to apps/as/app/(main)/(with-base-layout)/layout.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx b/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx new file mode 100644 index 000000000..c7b7ad4b4 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx @@ -0,0 +1,3 @@ +import { MyPbadmsSprtListPage } from "@/pages/my-pbadms-sprt"; + +export default MyPbadmsSprtListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx new file mode 100644 index 000000000..13df22b72 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/nti-hstry/page.tsx @@ -0,0 +1,3 @@ +import { NtiHstryListPage } from "@/pages/nti-hstry"; + +export default NtiHstryListPage; diff --git a/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx b/apps/as/app/(main)/(with-base-layout)/rvw-mnpw-mng/page.tsx similarity index 100% rename from apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx rename to apps/as/app/(main)/(with-base-layout)/rvw-mnpw-mng/page.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx new file mode 100644 index 000000000..72492a84c --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx @@ -0,0 +1,13 @@ +import { SewerBscPlanEditPage } from "@/pages/sewer-bsc-plan"; + +interface PageProps { + params: Promise<{ id: string }>; + searchParams: Promise<{ chgCycl?: string }>; +} + +export default async function Page({ params, searchParams }: PageProps) { + const { id } = await params; + const { chgCycl } = await searchParams; + const propChgCycl = chgCycl ? Number(chgCycl) : undefined; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx new file mode 100644 index 000000000..dec6502a8 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/[id]/page.tsx @@ -0,0 +1,13 @@ +import { SewerBscPlanDetailGate } from "@/pages/sewer-bsc-plan"; + +interface PageProps { + params: Promise<{ id: string }>; + searchParams: Promise<{ chgCycl?: string }>; +} + +export default async function Page({ params, searchParams }: PageProps) { + const { id } = await params; + const { chgCycl } = await searchParams; + const propChgCycl = chgCycl ? Number(chgCycl) : undefined; + return ; +} diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/page.tsx new file mode 100644 index 000000000..d07ca4b43 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/page.tsx @@ -0,0 +1,3 @@ +import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan"; + +export default SewerBscPlanListPage; diff --git a/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/regist/page.tsx b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/regist/page.tsx new file mode 100644 index 000000000..276160546 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/sewer-bsc-plan/regist/page.tsx @@ -0,0 +1,19 @@ +import { SewerBscPlanRegistPage } from "@/pages/sewer-bsc-plan"; + +interface PageProps { + searchParams: Promise<{ + taskSeComCd?: string; + taskSeCdNm?: string; + sewerBscPlanSeComCd?: string; + sewerBscPlanSeCdNm?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }>; +} + +export default async function Page({ searchParams }: PageProps) { + const params = await searchParams; + + return ; +} diff --git a/apps/as/app/layout.tsx b/apps/as/app/(main)/layout.tsx similarity index 84% rename from apps/as/app/layout.tsx rename to apps/as/app/(main)/layout.tsx index 3b64ab52f..dacee7f25 100644 --- a/apps/as/app/layout.tsx +++ b/apps/as/app/(main)/layout.tsx @@ -6,8 +6,8 @@ import { Providers } from "@/app/providers"; import "@/app/styles/globals.css"; export const metadata: Metadata = { - title: "행정지원시스템", - description: "행정지원시스템", + title: "디지털 행정지원 시스템", + description: "디지털 행정지원 시스템", }; interface RootLayoutProps { diff --git a/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx b/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx deleted file mode 100644 index c22d0f0b4..000000000 --- a/apps/as/app/(with-base-layout)/acntg-code-management/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AcntgCodeManagementPage as default } from "@/pages/acntg-code-management"; diff --git a/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx b/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx deleted file mode 100644 index d20a49e4f..000000000 --- a/apps/as/app/(with-base-layout)/cnstp-code-management/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CnstpCodeManagementPage as default } from "@/pages/cnstp-code-management"; diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx deleted file mode 100644 index baf4de7af..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/modify/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { CnfmModifyPage } from "@/pages/instl-sncn-old"; - -export default async function InstlSncnModifyPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - return ; -} diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx deleted file mode 100644 index 92769d3ef..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn-old/[id]/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { CnfmViewPage } from "@/pages/instl-sncn-old"; - -export default async function InstlSncnViewPage({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - return ; -} diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx deleted file mode 100644 index 6c1e37f16..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn-old/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { CnfmListPage } from "@/pages/instl-sncn-old"; - -export default CnfmListPage; diff --git a/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx deleted file mode 100644 index 82a838f8a..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn-old/register/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { CnfmRegistPage } from "@/pages/instl-sncn-old"; - -export default function Page(props: any) { - return ; -} diff --git a/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx b/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx deleted file mode 100644 index 476d0b7c2..000000000 --- a/apps/as/app/(with-base-layout)/instl-sncn/regist/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { InstlRegistPage } from "@/pages/instl-sncn"; - -interface InstlSncnRegistRouteProps { - searchParams: Promise<{ taskSeComCd?: string }>; -} - -export default async function Page({ searchParams }: InstlSncnRegistRouteProps) { - const params = await searchParams; - const taskSeComCd = params.taskSeComCd - ? params.taskSeComCd.split(",").filter(Boolean) - : undefined; - - return ; -} diff --git a/apps/as/app/(with-base-layout)/page.tsx b/apps/as/app/(with-base-layout)/page.tsx deleted file mode 100644 index 82a918b59..000000000 --- a/apps/as/app/(with-base-layout)/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MainPage as default } from "@/pages/home"; diff --git a/apps/as/app/api/auth/logout/route.ts b/apps/as/app/api/auth/logout/route.ts new file mode 100644 index 000000000..3e53a0705 --- /dev/null +++ b/apps/as/app/api/auth/logout/route.ts @@ -0,0 +1,10 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config"; + +export async function POST() { + const cookieStore = await cookies(); + cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN }); + return NextResponse.json({ message: "Logged out" }, { status: 200 }); +} diff --git a/apps/as/app/api/proxy/[...url]/route.ts b/apps/as/app/api/proxy/[...url]/route.ts index 15abf4c72..d54f08656 100644 --- a/apps/as/app/api/proxy/[...url]/route.ts +++ b/apps/as/app/api/proxy/[...url]/route.ts @@ -5,6 +5,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + COOKIE_DOMAIN, REFRESH_TOKEN, SERVER_API_URL, SITE_ID, @@ -20,6 +21,10 @@ async function handler( "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; + const deviceId = request.headers.get("X-Device-Id"); + if (deviceId) { + headers["X-Device-Id"] = deviceId; + } let body = {}; const isRefreshHandler = params.url.includes("refresh"); @@ -39,27 +44,13 @@ async function handler( } const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`; - console.log(`[Proxy API] ${method} ${params.url.join("/")} -> ${targetUrl}`); - console.log(`[Proxy API] Headers:`, headers); - if (!isRefreshHandler) { - console.log(`[Proxy API] Body:`, body); - } - const result = await fetch(targetUrl, { method, headers, body: JSON.stringify(body), }); - console.log(`[Proxy API] Response Status: ${result.status}`); - console.log(`[Proxy API] Response Headers:`, { - [ACCESS_TOKEN]: result.headers.get(ACCESS_TOKEN), - [REFRESH_TOKEN]: result.headers.get(REFRESH_TOKEN) ? '***hidden***' : undefined, - [AUTH_USER_ID]: result.headers.get(AUTH_USER_ID), - }); - if (!result.ok) { - console.error(`[Proxy API] Request failed with status ${result.status}`); return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 }); } @@ -68,24 +59,16 @@ async function handler( const userId = result.headers.get(AUTH_USER_ID); if (accessToken && userId) { - console.log(`[Proxy API] Login successful for user: ${userId}`); const response = NextResponse.json({ [ACCESS_TOKEN]: accessToken, [AUTH_USER_ID]: userId, }); - const forwardedHost = request.headers.get("x-forwarded-host"); - const host = request.headers.get("host"); - const actualHost = forwardedHost || host || new URL(request.url).hostname.toString(); - - // 운영 환경에서 domain 설정 - const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost; - if (refreshToken) { cookieStore.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, sameSite: "strict", - domain: domain, + domain: COOKIE_DOMAIN, path: "/", }); } diff --git a/apps/as/app/global-not-found.tsx b/apps/as/app/global-not-found.tsx new file mode 100644 index 000000000..6f66c97a6 --- /dev/null +++ b/apps/as/app/global-not-found.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import Image from "next/image"; + +import "@/app/styles/globals.css"; + +export const metadata: Metadata = { + title: "디지털 행정지원 시스템", + description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.", +}; + +export default function GlobalNotFound() { + return ( + + +
+
+

+ 디지털 행정지원 시스템에 오신 것을 환영합니다. +

+

오류가 발생하였습니다.

+
+ +
+ +

페이지가 존재하지 않습니다.

+

관리자에게 문의하여 주십시오

+
+
+ + + ); +} diff --git a/apps/as/docs/pwrk-cnsltn/개발가이드.md b/apps/as/docs/pwrk-cnsltn/개발가이드.md new file mode 100644 index 000000000..bedc4ef3b --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/개발가이드.md @@ -0,0 +1,800 @@ +# 실무협의문서 개발 가이드 + +> **대상 앱**: `frontend/apps/as` +> **작성일**: 2026-08-27 +> **버전**: 1.0 +> **SoT (백엔드)**: +> +> - `PwrkCnsltnDocCrtRsltController` — `/api/v1/pwrk-cnsltn-doc-crt-rslt` +> - `PwrkCnsltnMngController` — `/api/v1/pwrk-cnsltn-mng` +> - `DcRagApiToController` — `/api/v1/dcrag-to` +> - 초안생성 데이터 설명 (`02.03.초안생성_데이터_설명.md`) +> +> **현재 프론트 상태**: `pwrk-cnsltn-`\* 엔티티·화면 **미구현**. 재원협의(`fnrs-cnsltn`)만 존재한다. + +--- + +## 1. 문서 목적 + +재원협의 접수자가 **자료 검증 → 실무협의서 초안 생성 → 한글 웹기안기 열람/다운로드 → 편집본 버전 저장**까지 구현할 때 사용하는 프론트엔드 개발 가이드이다. + +백엔드 컨트롤러·DTO를 **API source of truth**로 두고, 초안생성 데이터 설명의 업무 시퀀스를 화면 동작에 매핑한다. + +--- + +## 2. 도메인 개요 + +실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 문서번호 규칙: + +| 문서 | 번호 예시 | 필드 | +| -------- | ----------------- | ----------------------------- | +| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` | +| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` | + +재원협의 1건당 실무협의서는 **1건만** 생성된다. `POST /pwrk-cnsltn-mng`는 `upDocNo`로 기존 문서를 재사용하고, 없으면 문서번호를 발급한다. + +### 2-1. 핵심 식별자 + +| 필드 | 의미 | 비고 | +| ----------------- | ------------------------------ | --------------------------------------------------------- | +| `pwrkCnsltnDocNo` | 실무협의문서번호 | 문서 PK. `P-YYYYMMDD-NNNN` | +| `upDocNo` | 상위문서번호(재원협의) | `upDocSeComCd = "FNRS"` | +| `crtDmndUuid` | 생성요청 UUID | 초안 생성 작업·결과·다운로드의 키 | +| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` | +| `orgnlOtptDocCn` | 원본출력문서내용(HWP 바이너리) | **목록/상세 응답에서는 항상** `null`. 다운로드 API만 제공 | + +### 2-2. IIPBS 약어 + +| 한글 | 약어 | 필드 예 | +| -------- | ------ | ----------------------------- | +| 실무 | PWRK | `pwrkCnsltnDocNo` | +| 협의 | CNSLTN | `pwrk-cnsltn-mng` | +| 관리 | MNG | `PwrkCnsltnMng` | +| 문서 | DOC | `pwrkCnsltnDocNo` | +| 생성 | CRT | `crtDmndUuid`, `crtSttsComCd` | +| 요청 | DMND | `crtDmndUuid` | +| 결과 | RSLT | `crtRslt` | +| 버전 | VER | `verSn` | +| 일련번호 | SN | `verSn` | +| 원본 | ORGNL | `orgnlOtptDocCn` | +| 출력 | OTPT | `orgnlOtptDocCn` | +| 내용 | CN | `orgnlOtptDocCn` | +| 완료 | CMPTN | `crtCmptnDt` | +| 일시 | DT | `crtCmptnDt` | +| 검증 | VRFC | `vrfcSttsComCd` | +| 전송 | TRSM | `trsmFileList` | +| 보완 | SPLMNT | `splmntDmndList` | +| 요청자 | RQSTR | `rqstrUuid`, `rqstrNm` | + +--- + +## 3. 전체 업무 흐름 + +재원협의 **접수자**가 실무협의서 초안을 만드는 경로이다. 프론트는 DC RAG를 직접 호출하지 않고 **as-service**만 호출한다. + +```mermaid +sequenceDiagram + autonumber + participant User as 재원협의 접수자 + participant FE as AS Frontend + participant AS as as-service + participant RAG as DC RAG + + User->>FE: [자료 검증 요청] + FE->>AS: POST /dcrag-to/documents/transfer
{ fnrsCnsltnDocNo } + AS->>RAG: 자료전송 (IF-01) + AS-->>FE: { resultCode: SUCCESS, requestId } + Note over FE: 재원협의 상세 재조회 + AS-->>FE: esntlFileList / etcFileList 에
vrfcDt, vrfcRsltCn, vrfcSttsComCd + + User->>FE: [초안 생성] + FE->>AS: POST /pwrk-cnsltn-mng
{ upDocNo, upDocSeComCd: FNRS, prgrsSttsComCd: DV } + alt 실무협의서 번호 없음 + AS->>AS: P-번호 발급 + 문서 등록 + else 이미 존재 + AS->>AS: 기존 pwrkCnsltnDocNo 재사용 + end + AS->>RAG: 초안생성 비동기 요청 (IF-03-1) + AS-->>FE: 201 { pwrkCnsltnDocNo, crtDmndUuid, crtSttsComCd: R, estimatedMinutes: 10 } + + loop 약 10분, status !== COMPLETED/FAILED + FE->>AS: GET /dcrag-to/documents/draft/status/{crtDmndUuid} + AS-->>FE: { status, events[], progressPercent } + end + + Note over FE: events 에 COMPLETED 가 있으면
웹기안기 열기 / 다운로드 가능 + + User->>FE: [문서 열기] 또는 [다운로드] + FE->>AS: GET /pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open
또는 /download + + User->>FE: 웹기안기 편집 후 [저장] + FE->>AS: POST /pwrk-cnsltn-doc-crt-rslt (multipart)
crtDmndUuid + file + AS-->>FE: 201 { verSn: N+1, ... } +``` + +### 3-1. 프론트가 호출하면 안 되는 API + +| API | 이유 | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `POST /dcrag-to/documents/draft/generate` | 초안 생성은 `POST /pwrk-cnsltn-mng`가 문서번호 발급 + DC RAG 호출을 **한 트랜잭션**으로 처리한다. 프론트는 관리 API만 호출한다. | + +--- + +## 4. 화면 데이터 흐름 + +초안 생성은 **재원협의 상세**에서 시작하고, 실무협의 목록/상세는 별도 화면이다. + +``` +BEGIN + + // ===== 재원협의 상세 (기존 fnrs-cnsltn view) ===== + CALL findById(fnrsCnsltnDocNo) + DISPLAY 재원협의 상세 + 필수/기타 첨부 + + ON [자료 검증 요청] 버튼 CLICK DO + CALL requestDocumentTransfer({ fnrsCnsltnDocNo }) + CALL findById(fnrsCnsltnDocNo) // 검증결과 필드 반영 + DISPLAY 첨부 리스트 (vrfcSttsComCd: PASS | FAIL) + END ON + + ON [초안 생성] 버튼 CLICK DO + CALL pwrkCnsltnMngService.save({ + upDocNo: fnrsCnsltnDocNo, + upDocSeComCd: "FNRS", + prgrsSttsComCd: "DV" + }) + // → pwrkCnsltnDocNo, crtDmndUuid, estimatedMinutes + START POLL getDraftGenerationStatus(crtDmndUuid) + DISPLAY 초안 생성 진행률 (약 10분) + END ON + + ON POLL status = COMPLETED 또는 events.eventType 에 COMPLETED DO + ENABLE [웹기안기 열기] [다운로드] + END ON + + ON [웹기안기 열기] 버튼 CLICK DO + CALL HwpCtrl.Open( webhwpOpenUrl(crtDmndUuid) ) // TBD: 컨트롤러 미구현 + END ON + + ON [다운로드] 버튼 CLICK DO + CALL downloadLatest(crtDmndUuid) // blob + Content-Disposition + SAVE 로컬 파일 + END ON + + // ===== 실무협의 목록 ===== + CALL pwrkCnsltnMngService.findPage(searchDto + pageable) + DISPLAY 실무협의 문서 그리드 + + ON 행 CLICK DO + NAVIGATE 실무협의 상세 (pwrkCnsltnDocNo) + END ON + + // ===== 실무협의 상세 / 재원협의에서 연계 조회 ===== + CALL findById(pwrkCnsltnDocNo) + // 또는 재원협의 번호로: findByUpDocNo(fnrsCnsltnDocNo) + DISPLAY 마스터 + cnsltnList + pjtcoAjmtList + crtDmndList.crtRslt + // crtRslt.orgnlOtptDocCn 은 항상 null — 바이너리는 download API + + ON 버전 이력 조회 DO + CALL pwrkCnsltnDocCrtRsltService.findPage({ crtDmndUuid, sort: "verSn,desc" }) + DISPLAY 버전 목록 + END ON + + ON 웹기안기 편집 완료 [저장] DO + CALL saveMultipart({ crtDmndUuid, file }) // 서버가 verSn + 1 + INVALIDATE 상세·버전 목록 + END ON + +END +``` + +--- + +## 5. API 명세 + +### 5-1. 공통 + +| 항목 | 값 | +| ----------- | ----------------------------------------------------------------- | +| Gateway | `/as-service` | +| Base prefix | `ENDPOINT.AS_SERVICE.*` (`apps/as/src/shared/config/api.ts`) | +| Auth 헤더 | `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터) | +| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` | + +추가할 ENDPOINT: + +```ts +AS_SERVICE: { + // 기존 … + PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng", + PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt", + DCRAG_TO: "/as-service/api/v1/dcrag-to", +} +``` + +--- + +### 5-2. DC RAG 연계 (`DcRagApiToController`) + +프론트 엔티티 슬라이스: `entities/dcrag-to/` + +| 기능 | Controller | Method | URL | Request | Response | +| ----------------- | -------------------------- | ------ | ------------------------------------------------ | --------------------------------- | ------------------------------------- | +| 자료전송요청 | `requestDocumentTransfer` | POST | `/dcrag-to/documents/transfer` | `DcRagDocumentTransferRequestDto` | `DcRagCommonResponseDto` | +| 초안생성 진행상태 | `getDraftGenerationStatus` | GET | `/dcrag-to/documents/draft/status/{crtDmndUuid}` | path | `DcRagDraftStatusResponseDto` | +| 금액검증요청 | `requestCostVerification` | POST | `/dcrag-to/documents/cost/verify` | `DcRagCostVerifyRequestDto` | `DcRagCommonResponseDto` (202) | +| 초안생성요청 | `requestDraftGeneration` | POST | `/dcrag-to/documents/draft/generate` | `DcRagDraftGenerateRequestDto` | `DcRagDraftGenerateResponseDto` (202) | + +**화면에서 쓰는 것**: 자료전송 + 상태 폴링 (+ 추후 금액검증). +초안생성요청은 5-3의 `pwrkCnsltnMngService.save`로 대체한다. + +#### 자료전송 Request / Response + +```json +POST /as-service/api/v1/dcrag-to/documents/transfer +{ "fnrsCnsltnDocNo": "F-20260824-0001" } +``` + +```json +{ + "resultCode": "SUCCESS", + "message": "자료전송요청 접수 완료", + "requestId": "F-20260824-0001" +} +``` + +자료 검증이 **완료된 뒤** 재원협의 상세를 다시 조회하면, 필수첨부(`esntlFileList`)·기타첨부(`etcFileList`) 각 파일에 검증 결과가 붙는다. + +| 필드 | 예 | 의미 | +| --------------- | ----------------------------- | ------------ | +| `vrfcDt` | `2026-08-25 18:13:49` | 검증일시 | +| `vrfcRsltCn` | `정상 확인 (재원협의 신청서)` | 검증결과내용 | +| `vrfcSttsComCd` | `PASS` / `FAIL` | 검증상태 | + +프론트 `FnrsCnsltnMngDetResponseDto` / `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 위 3필드가 **아직 없다**. 재원협의 상세 첨부 영역에 배지를 그리려면 타입을 확장해야 한다. + +#### 초안생성 상태 폴링 + +```http +GET /as-service/api/v1/dcrag-to/documents/draft/status/{crtDmndUuid} +``` + +| 필드 | 설명 | +| ----------------------- | --------------------------------------------------------------------------------- | +| `status` | `REQUESTED` / `DOWNLOADING` / `ANALYZING` / `GENERATING` / `COMPLETED` / `FAILED` | +| `statusName` | 표시명 (예: `생성 완료`) | +| `progressPercent` | 0~100 | +| `events[]` | `{ eventType, eventMessage, occurredDt }` | +| `startedDt` | 시작일시 | +| `estimatedCompletionDt` | 예상 완료일시 | +| `errorMessage` | 실패 시 메시지 | + +폴링 규칙: + +- 주기: **5~10초** (TBD, UX 확정). +- 예상 소요: **약 10분** (`estimatedMinutes`). +- 종료 조건: `status === "COMPLETED"` **또는** `events`에 `eventType === "COMPLETED"`. +- 실패: `status === "FAILED"` → 폴링 중단, `errorMessage` 표시. +- 완료 전에는 웹기안기 열기·다운로드 버튼을 **비활성**. + +이벤트 순서 예시: + +| eventType | eventMessage | +| ------------- | --------------------------- | +| `REQUESTED` | 요청을 접수했습니다. | +| `DOWNLOADING` | 신청서를 수신하는 중입니다. | +| `ANALYZING` | 자료 준비가 완료되었습니다. | +| `GENERATING` | 문서 저장이 완료되었습니다. | +| `COMPLETED` | 문서 생성이 완료되었습니다. | + +--- + +### 5-3. 실무협의관리 (`PwrkCnsltnMngController`) + +프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-mng/` + +| 기능 | Controller | Method | URL | Request | Response | +| ------------------- | ---------------------- | ------ | ----------------------------------------------------- | ----------------------------------- | ------------------------------------ | +| 페이징 조회 | `findPage` | GET | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSearchDto` + Pageable | `Page` | +| 상세 (문서번호) | `findById` | GET | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | `PwrkCnsltnMngDetResponseDto` | +| 상세 (재원협의번호) | `findByUpDocNo` | GET | `/pwrk-cnsltn-mng/updocno/{upDocNo}` | path | `PwrkCnsltnMngDetResponseDto` | +| 등록 + 초안생성 | `save` | POST | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSaveRequestDto` | `PwrkCnsltnMngSaveResponseDto` (201) | +| 신청 통합 저장 | `saveApply` | POST | `/pwrk-cnsltn-mng/save-apply` | `PwrkCnsltnApplySaveRequestDto` | `string` (문서번호, 201) | +| 수정 | `update` | PUT | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | `PwrkCnsltnMngUpdateRequestDto` | `string` | +| 삭제 | `delete` → `remove` | DELETE | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | 204 | +| 보완완료 제출 | `submitSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/submitsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` | +| 보완요청 등록 | `submitReqSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/reqsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` | +| 진행상태 수정 | `changePrgrsSttsComCd` | POST | `/pwrk-cnsltn-mng/doc/updatePrgrsStts` | `DocPrgrsUpdateRequestDto` | 200 | + +목록 기본 호출: + +```http +GET /as-service/api/v1/pwrk-cnsltn-mng?page=0&size=10&sort=pwrkCnsltnDocNo,desc +``` + +#### 초안 생성 Request / Response (`save`) + +```json +POST /as-service/api/v1/pwrk-cnsltn-mng +{ + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV" +} +``` + +```json +{ + "pwrkCnsltnDocNo": "P-20260826-0001", + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV", + "resultCode": "ACCEPTED", + "message": "초안 생성 작업이 접수되었습니다.", + "crtDmndUuid": "382f2821-9231-42e6-8309-4a1718a2ead8", + "crtSttsComCd": "R", + "requestId": "382f2821-9231-42e6-8309-4a1718a2ead8", + "estimatedMinutes": 10 +} +``` + +백엔드 동작 (`PwrkCnsltnMngService.save`): + +1. `upDocNo`로 기존 실무협의서가 있으면 **그 번호를 재사용** (재원협의 1 : 실무협의 1). +2. 없으면 `docNoMngService.issueDocNo(PWRK)`로 `P-` 번호 발급 후 insert. +3. DC RAG 초안생성을 비동기로 호출하고 `crtDmndUuid`를 응답에 실어 반환. +4. DC RAG 실패 시에도 **문서는 롤백하지 않고** 로그만 남긴다. 프론트는 `resultCode`가 `ACCEPTED`/`SUCCESS`가 아니면 토스트로 실패를 알린다. + +#### 상세 응답 구조 (`PwrkCnsltnMngDetResponseDto`) + +재원협의 상세에서 `GET .../updocno/{fnrsCnsltnDocNo}`로 연계 데이터를 한 번에 가져온다. + +```json +{ + "pwrkCnsltnDocNo": "P-20260827-0001", + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV", + "cnsltnList": [], + "pjtcoAjmtList": [], + "crtDmndList": [ + { + "crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a", + "pwrkCnsltnDocNo": "P-20260827-0001", + "rqstrUuid": "65a00f65-8460-49af-98ec-042977e56f4b", + "rqstrNm": "관리자", + "crtSttsComCd": "C", + "crtRslt": { + "crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a", + "verSn": 1, + "rgtrUuid": "...", + "rgtrNm": "관리자", + "orgnlOtptDocCn": null, + "crtCmptnDt": "2026-08-27 10:47:01" + } + } + ], + "splmntDmndList": [], + "trsmFileList": [] +} +``` + +`crtRslt`는 최신 버전 메타만 포함한다. `orgnlOtptDocCn`은 절대 쓰지 말고 download API를 호출한다. + +마스터에 재원협의 사업정보가 조인된다: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등. + +--- + +### 5-4. 문서생성결과 (`PwrkCnsltnDocCrtRsltController`) + +프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-doc-crt-rslt/` +복합 PK: `crtDmndUuid` + `verSn` + +| 기능 | Controller | Method | URL | Request | Response | +| ------------------ | ------------------------- | ------ | ---------------------------------- | ----------------------------------------------- | --------------------------------------- | +| 버전 이력 페이징 | `findPage` | GET | `/pwrk-cnsltn-doc-crt-rslt` | `PwrkCnsltnDocCrtRsltSearchDto` + Pageable | `Page` | +| 특정 버전 상세 | `findById` | GET | `/{crtDmndUuid}/{verSn}` | path | `PwrkCnsltnDocCrtRsltResponseDto` | +| 최신 버전 상세 | `findLatestByCrtDmndUuid` | GET | `/{crtDmndUuid}/latest` | path | `PwrkCnsltnDocCrtRsltResponseDto` | +| 특정 버전 다운로드 | `download` | GET | `/{crtDmndUuid}/{verSn}/download` | path | `byte[]` (attachment) | +| 최신 버전 다운로드 | `downloadLatest` | GET | `/{crtDmndUuid}/download` | path | `byte[]` (attachment) | +| 등록 (multipart) | `saveMultipart` | POST | `/` `multipart/form-data` | `PwrkCnsltnDocCrtRsltSaveRequestDto` + `file` | `PwrkCnsltnDocCrtRsltResponseDto` (201) | +| 등록 (JSON) | `saveJson` | POST | `/` `application/json` | `PwrkCnsltnDocCrtRsltSaveRequestDto` | 201 | +| 수정 (multipart) | `updateMultipart` | PUT | `/{crtDmndUuid}/{verSn}` multipart | `PwrkCnsltnDocCrtRsltUpdateRequestDto` + `file` | ResponseDto | +| 수정 (JSON) | `updateJson` | PUT | `/{crtDmndUuid}/{verSn}` json | UpdateRequestDto | ResponseDto | +| 삭제 | `delete` → `remove` | DELETE | `/{crtDmndUuid}/{verSn}` | path | 204 | + +Spring 매핑 주의: `/{crtDmndUuid}/latest` 와 `/{crtDmndUuid}/download` 는 `/{crtDmndUuid}/{verSn}` 보다 **구체 경로가 우선**한다. 프론트 path를 섞지 말 것. + +버전 이력 조회 예: + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc +``` + +#### 다운로드 (attachment) + +화면 [다운로드] 버튼 → PC 저장. + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/download +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/download +``` + +- axios: `responseType: "blob"`. +- 파일명: `Content-Disposition`의 `filename*=UTF-8''...` (`parseFilenameFromResponse` 재사용). +- 서버 파일명 규칙: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`. +- **현재 컨트롤러 Content-Type**: `application/x-hwp`. + 데이터 설명 예시는 `application/haansofthwpx`. 프론트는 Content-Type에 의존하지 말고 blob으로 저장한다. + +기존 `attachService` 다운로드와 동일 패턴: + +```ts +const response = await client.get(url, { responseType: "blob" }); +const filename = parseFilenameFromResponse(response); +const objectUrl = window.URL.createObjectURL(response.data); +``` + +#### 웹기안기 열기 (inline) — TBD + +데이터 설명 기준 엔드포인트. **현재** `PwrkCnsltnDocCrtRsltController`**에는 없음.** 백엔드 추가 후 연결한다. + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/webhwp/open +``` + +의도: + +- `Content-Disposition: inline` +- 한컴 웹기안기 `HwpCtrl.Open(url)` 이 이 URL을 직접 호출 +- 브라우저 다운로드 창이 뜨지 않고 캔버스에 로드 + +프론트는 **blob fetch 후 Object URL을 넘기지 않는다.** 인증 헤더가 필요한 절대 URL을 기안기에 전달하는 방식이므로, 쿠키/게이트웨이 인증 가능 여부를 백엔드와 확인한다 (TBD). + +#### 신규 버전 등록 (웹기안기 저장) + +서버가 최신 `verSn + 1`을 부여한다. 클라이언트가 `verSn`을 보내지 않는다. + +```http +POST /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt +Content-Type: multipart/form-data + +crtDmndUuid: {uuid} +file: 재원협의 실무협의서_v2.hwpx +``` + +화면에서는 **multipart만** 사용한다. JSON `saveJson`은 바이너리를 body에 넣는 경로라 UI에서 쓰지 않는다. + +`file` part 이름: `"file"`. DTO 필드는 `@ModelAttribute` 로 평문 필드(`crtDmndUuid`) 또는 `saveDto` part. + +--- + +## 6. DTO 매핑 (프론트 타입명 = 백엔드 클래스명) + +### 6-1. `PwrkCnsltnDocCrtRslt*` + +| 계층 | 파일 | 정의 | +| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- | +| Save Request | `pwrk-cnsltn-doc-crt-rslt-payload-schema.ts` | zod SSOT → `PwrkCnsltnDocCrtRsltSaveRequestDto` | +| Response / Search / Update / Service | `pwrk-cnsltn-doc-crt-rslt-interface.ts` | `interface` | + +**Response** + +| 필드 | 타입 | 비고 | +| ---------------- | -------- | --------------------- | +| `crtDmndUuid` | `string` | UUID | +| `verSn` | `number` | BigDecimal | +| `rgtrUuid` | `string` | | +| `rgtrNm` | `string` | 서버 enrich | +| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 | +| `crtCmptnDt` | `string` | `yyyy-MM-dd HH:mm:ss` | + +**Search** (`PageableParams` 확장) + +| 필드 | 비고 | +| ----------------- | ---------------------- | +| `crtDmndUuid` | 버전 이력 조회 시 필수 | +| `pwrkCnsltnDocNo` | | +| `verSn` | | +| `rgtrUuid` | | + +**Save Request (zod)** + +| 필드 | 필수 | 비고 | +| ------------- | ---- | ------------------------------------ | +| `crtDmndUuid` | Y | | +| `rgtrUuid` | N | 미전송 시 서버가 로그인 사용자 사용 | +| `file` | UI | multipart `file` part. DTO 필드 아님 | + +**Update Request** + +| 필드 | 비고 | +| ------------ | --------- | +| `rgtrUuid` | | +| `crtCmptnDt` | | +| `file` | multipart | + +### 6-2. `PwrkCnsltnMng*` + +**Search** + +`pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm` + +**Save Request (초안 생성)** — 필드가 얇으므로 interface만으로도 가능. 권장은 zod. + +| 필드 | 필수 | 값 | +| ----------------- | ---- | ----------------- | +| `upDocNo` | Y | 재원협의 문서번호 | +| `upDocSeComCd` | | `"FNRS"` | +| `prgrsSttsComCd` | | `"DV"` (초안생성) | +| `pwrkCnsltnDocNo` | N | 서버 발급 | + +**Save Response** — `save`만 이 DTO. `fnrsCnsltnMngService.save`가 `string`을 반환하는 것과 **다름**. + +| 필드 | 의미 | +| ------------------ | --------------------------------- | +| `pwrkCnsltnDocNo` | 발급/재사용 번호 | +| `resultCode` | `ACCEPTED` / `SUCCESS` / 실패코드 | +| `crtDmndUuid` | 폴링 키 | +| `crtSttsComCd` | `R` 생성요청 | +| `estimatedMinutes` | 기본 10 | + +**DetResponse 중첩** + +- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …) +- `pjtcoAjmtList`: 사업비조정 +- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과 +- `splmntDmndList`: 보완요청 +- `sttsChgHstryList`: 상태변경이력 +- `trsmFileList`: 전송파일 (데이터 설명 기준. Det DTO Java 필드 확인 후 타입 반영) + +### 6-3. DC RAG + +```ts +interface DcRagDocumentTransferRequestDto { + fnrsCnsltnDocNo: string; + taskSeComCd?: string; + taskSeCdNm?: string; +} + +interface DcRagCommonResponseDto { + resultCode: string; + message?: string; + requestId?: string; +} + +interface DcRagDraftStatusResponseDto { + crtDmndUuid: string; + status: DraftStatus; + statusName?: string; + progressPercent?: number; + events?: EventInfoDto[]; + startedDt?: string; + estimatedCompletionDt?: string; + errorMessage?: string | null; +} + +type DraftStatus = + | "REQUESTED" + | "DOWNLOADING" + | "ANALYZING" + | "GENERATING" + | "COMPLETED" + | "FAILED"; +``` + +--- + +## 7. 공통코드 + +| 상위코드 | 필드 | 값 | 명칭 | +| ----------------------- | ---------------- | --------------- | ------------- | +| `ASDOCSE` | `upDocSeComCd` | `FNRS` | 재원협의 | +| `ASDOCSE` | 문서구분 | `PWRK` | 실무협의 | +| `PWRKSTTS` | `prgrsSttsComCd` | `DV` | 초안생성 | +| `PWRKSTTS` | | `AR` | 검토자지정 | +| `PWRKSTTS` | | `SV` | 작성중 | +| `PWRKSTTS` | | `RB` | 검토회신 | +| `PWRKSTTS` | | `RC` | 협의회신 | +| `PWRKSTTS` | | `TR` | 검토결과 회신 | +| `PWRKSTTS` | | `SL` | 보완요청 | +| `PWRKSTTS` | | `AA` | 금액조정중 | +| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` | 생성요청 | +| | | `I` | 생성중 | +| | | `C` | 생성완료 | +| | | `F` | 생성실패 | +| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` | | + +`CrtDmndResponseDto`는 `@CodeName(upCd = "CRTSTTS")`, 생성요청 단건 DTO는 `AICRTSTTS`를 쓴다. 코드값이 `R/I/C/F`로 같으므로 UI는 코드값으로 분기하고, 표시명은 응답의 `*Nm` 필드를 쓴다. + +초안 생성 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 상세의 `crtSttsComCd = C`. + +--- + +## 8. 프론트엔드 구현 가이드 + +### 8-1. 기술 스택·원칙 + +- App Router + FSD (`app/` → `pages/` → `widgets/` → `entities/` → `shared/`) +- React Query. ad-hoc `useQuery` + service 직접 호출 금지 +- `@common/components`, `useDataTable`, `@tanstack/react-table` +- `'use client'`는 Web API(다운로드, 폴링, 웹기안기)가 있는 페이지만 +- 함수명 = Controller public 메서드명 (`delete` → `remove`) +- 타입명 = 백엔드 DTO명. 프론트 전용 축약 금지 +- Save/Apply Request = zod SSOT. Response는 `interface` + +참조 슬라이스: `entities/fnrs-cnsltn/`, `entities/sewer-bsc-plan/` + +### 8-2. 모듈·파일 배치 + +| 구분 | 경로 | +| -------------------- | ------------------------------------------------------------------- | +| ENDPOINT | `apps/as/src/shared/config/api.ts` | +| 실무협의 관리 엔티티 | `entities/pwrk-cnsltn-mng/` | +| 생성결과 엔티티 | `entities/pwrk-cnsltn-doc-crt-rslt/` | +| DC RAG 엔티티 | `entities/dcrag-to/` | +| 목록 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/page.tsx` | +| 상세 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/[id]/page.tsx` | +| 페이지 UI | `pages/pwrk-cnsltn/` | +| 재원협의 상세 연계 | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` (버튼·폴링·첨부 검증배지) | + +엔티티 내부: + +``` +entities/pwrk-cnsltn-mng/ +├── model/ +│ ├── pwrk-cnsltn-mng-interface.ts +│ └── pwrk-cnsltn-mng-payload-schema.ts +├── api/ +│ ├── pwrk-cnsltn-mng-service.ts +│ └── queries.ts +└── index.ts + +entities/pwrk-cnsltn-doc-crt-rslt/ +├── model/ +│ ├── pwrk-cnsltn-doc-crt-rslt-interface.ts +│ └── pwrk-cnsltn-doc-crt-rslt-payload-schema.ts +├── api/ +│ ├── pwrk-cnsltn-doc-crt-rslt-service.ts +│ └── queries.ts +└── index.ts +``` + +### 8-3. Service 시그니처 요지 + +`pwrkCnsltnMngService.save` 반환 타입이 `string`이 아니라 `PwrkCnsltnMngSaveResponseDto` 인 점을 `fnrs-cnsltn`과 혼동하지 말 것. + +```ts +export interface PwrkCnsltnMngService { + findPage(searchDto: PwrkCnsltnMngSearchDto): Promise>; + findById(pwrkCnsltnDocNo: string): Promise; + findByUpDocNo(upDocNo: string): Promise; + save(body: PwrkCnsltnMngSaveRequestDto): Promise; + saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise; + update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise; + submitSupplement(pwrkCnsltnDocNo: string, body: PwrkCnsltnApplySaveRequestDto): Promise; + submitReqSupplement( + pwrkCnsltnDocNo: string, + body: PwrkCnsltnApplySaveRequestDto, + ): Promise; + remove(pwrkCnsltnDocNo: string): Promise; + changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise; +} + +export interface PwrkCnsltnDocCrtRsltService { + findPage( + searchDto: PwrkCnsltnDocCrtRsltSearchDto, + ): Promise>; + findById(crtDmndUuid: string, verSn: number): Promise; + findLatestByCrtDmndUuid(crtDmndUuid: string): Promise; + download(crtDmndUuid: string, verSn: number): Promise>; + downloadLatest(crtDmndUuid: string): Promise>; + saveMultipart(formData: FormData): Promise; + updateMultipart( + crtDmndUuid: string, + verSn: number, + formData: FormData, + ): Promise; + remove(crtDmndUuid: string, verSn: number): Promise; +} +``` + +multipart `Content-Type`은 axios가 boundary를 붙이도록 **헤더를 수동 지정하지 않는다.** + +### 8-4. 폴링 훅 + +`useQuery` + `refetchInterval`: + +```ts +useQuery({ + queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)], + queryFn: () => dcragToService.getDraftGenerationStatus(crtDmndUuid), + enabled: Boolean(crtDmndUuid) && isPolling, + refetchInterval: (query) => { + const status = query.state.data?.status; + if (status === "COMPLETED" || status === "FAILED") return false; + return 5_000; + }, +}); +``` + +`COMPLETED` 시 `pwrkCnsltnMng` 상세 쿼리를 invalidate 하여 `crtDmndList[].crtRslt`를 갱신한다. + +### 8-5. 재원협의 상세에 붙일 UI + +`pages/fnrs-cnsltn` 조회 모드: + +1. **자료 검증 요청** — `requestDocumentTransfer` → 상세 refetch → 첨부 행에 PASS/FAIL 배지 (`vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`). +2. **초안 생성** — 검증이 끝난 뒤 활성. `pwrkCnsltnMngService.save`. +3. **진행 표시** — `progressPercent` + 이벤트 타임라인. `estimatedMinutes`(10)를 안내. +4. **웹기안기 열기 / 다운로드** — `COMPLETED` 이후에만. + +`findByUpDocNo(fnrsCnsltnDocNo)`로 이미 초안이 있으면 `crtDmndUuid`를 복원해 폴링·열기를 이어간다. + +### 8-6. 실무협의 목록 컬럼 (권장) + +마스터 응답 + 재원협의 조인 필드 기준. 화면설계 확정 전 TBD. + +| 표시명 | Field | +| ------------ | ------------------ | +| 실무협의번호 | `pwrkCnsltnDocNo` | +| 재원협의번호 | `upDocNo` | +| 진행상태 | `prgrsSttsComCdNm` | +| 사업명 | `bizNm` | +| 시도/구군 | `sido` / `gugun` | +| 신청자 | `aplcntNm` | +| 등록일 | `regDt` | + +행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}`. + +--- + +## 9. 구현 순서 + +``` +Task Progress: +- [ ] 1. ENDPOINT 3개 추가 (PWRK_CNSLTN_MNG, PWRK_CNSLTN_DOC_CRT_RSLT, DCRAG_TO) +- [ ] 2. entities/dcrag-to (transfer + draft status) +- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, save) +- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (findPage, downloadLatest, saveMultipart) +- [ ] 5. 재원협의 상세: 검증결과 3필드 타입 확장 + 배지 +- [ ] 6. 재원협의 상세: 자료검증 / 초안생성 / 폴링 UI +- [ ] 7. 실무협의 목록·상세 라우트 +- [ ] 8. 다운로드 (blob) +- [ ] 9. 웹기안기 Open — 백엔드 webhwp/open 확인 후 +- [ ] 10. 웹기안기 저장 → saveMultipart (verSn + 1) +``` + +1~4는 엔티티 레이어만으로 먼저 닫는다. 화면은 5부터. + +--- + +## 10. TBD · 백엔드 확인 + +| 항목 | 내용 | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `webhwp/open` | 데이터 설명에는 있으나 **현재 Controller에 없음**. 추가 일정·인증 방식 확인 | +| Content-Type | Controller `application/x-hwp` vs 설명 `application/haansofthwpx` | +| 파일 확장자 | 다운로드 파일명이 `.hwp`로 고정. 실제 바이너리가 hwpx일 수 있음 | +| 폴링 주기 | 5초 vs 10초, 탭 비활성 시 중단 여부 | +| 검증 필드 타입 | `FnrsCnsltnMngDetResponseDto.esntlFileList` / `etcFileList` Java 타입이 아직 `FnrsCnsltnAtflResponseDto`가 아님. 런타임 JSON에 `vrfc*`가 오는지 확인 | +| `trsmFileList` | 데이터 설명 상세 예시에는 있으나 Det DTO Java 선언과 불일치 가능 | +| `crtSttsComCd` 상위코드 | `CRTSTTS` vs `AICRTSTTS` | +| 웹기안기 라이선스 URL | `HwpCtrl.Open`에 넘길 게이트웨이 절대 경로 (쿠키 인증 가능 여부) | +| 초안 생성 1회 제한 | `requestDraftGeneration` 주석에 “1회 제한”. 재클릭 UX (비활성 / 재요청 확인) | +| 자료전송 허용 확장자 | `.doc .docx .hwp .hwpx .md .pdf .txt .xls .xlsm .xlsx`. 이미지 첨부 시 DC RAG 실패 | + +--- + +## 11. 참고 + +| 자료 | 위치 | +| ---------------------- | ------------------------------------------------------------------------------ | +| 생성결과 컨트롤러 | `backend/.../pwrkcnsltndoccrtrslt/PwrkCnsltnDocCrtRsltController.java` | +| 실무협의관리 컨트롤러 | `backend/.../pwrkcnsltnmng/PwrkCnsltnMngController.java` | +| DC RAG TO 컨트롤러 | `backend/.../dcrag/DcRagApiToController.java` | +| 초안생성 데이터 설명 | `02.03.초안생성_데이터_설명.md` | +| 엔티티 구현 스킬 | `frontend/.cursor/skills/backend-controller-to-entity/` | +| 재원협의 엔티티 (패턴) | `apps/as/src/entities/fnrs-cnsltn/` | +| IIPBS 단어목록 | `frontend/docs/IIPBS_단어목록.md` | +| 진행상태 코드 | `backend/IIPBS_CS/docs/2026/05.행정지원/01.문서상태별_사용자_상태변경_이력.md` | diff --git a/apps/as/docs/sewer-bsc-plan/dmbp-regist-migration.md b/apps/as/docs/sewer-bsc-plan/dmbp-regist-migration.md new file mode 100644 index 000000000..e8f36ef2e --- /dev/null +++ b/apps/as/docs/sewer-bsc-plan/dmbp-regist-migration.md @@ -0,0 +1,674 @@ +# dmbpRegist.jsp → React 마이그레이션 분석·구현 가이드 + +| 항목 | 내용 | +| ----------- | ------------------------------------------------------ | +| 원본 JSP | `adssNewOld/.../fnrsc/dmbp/dmbpRegist.jsp` | +| 대상 앱 | `frontend/apps/as` | +| 도메인 | 하수도기본계획 (구: 기본계획검토 / dmbp) | +| API 엔티티 | `@/entities/sewer-bsc-plan` (`SewerBscPlanController`) | +| 참조 패턴 | `@/pages/fnrs-cnsltn` + `@/widgets/pbadms-sprt` | +| 문서 목적 | contents 영역 **섹션별** UI·상태·API 매핑 및 구현 방향 | +| 작성 기준일 | 2026-07-27 | + +--- + +## 1. 마이그레이션 범위 + +### 1-1. 포함 (contents만) + +JSP `568~1274`행: `
` ~ `` + +| 영역 | JSP 식별 | 개요 | +| ---- | ---------------------------------- | --------------------------------------------------------------- | +| A | `gTitle1` | 페이지 타이틀·브레드크럼 | +| B | `slide_10` | 기본계획변경 신청(마스터·연도·개요) | +| C | `slide_15` | 신청자·설계자 정보 | +| D | `slide_19` | 개인정보 동의 | +| E | `slide_11` | 사업개요(단계별 인구·보급률) | +| F | `slide_12` | 총사업비 — `BizCstMatrixFormSection` 전용 | +| G | `slide_13` | 재원분담 | +| H | `slide_14` | 처리시설별 하수도정비기본계획 (Checkbox + Tabs) | +| I | `slide_16` | 체크리스트 + 필수첨부 + 기타첨부 | +| J | JSP 상단 `saveBtn` / `presentnBtn` | React: 폼 하단 액션바 (`fnrs-form-page` 279~323과 동일 구성, API만 sewer) | + +### 1-2. 제외 + +- `<%@ include %>`: top / sideMenu / footer / `bscPlnPopup.jsp` / `sndrdBzAmnt.jsp` +- 레이아웃·메뉴·권한 셸 → 기존 `BaseLayout` / `PageLayout` / AUTH 체계 +- `mStep1` 진행 단계 표시 (미구현 · 마이그레이션 범위 밖) + +팝업(`이전기본계획조회`)은 **별도 Dialog 컴포넌트**로 재구현하며, include JSP 자체는 포팅하지 않는다. + +--- + +## 2. 목표 아키텍처 + +``` +app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx + └─ pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx + └─ sewer-bsc-plan-form-page.tsx # FormProvider + 섹션 조립 + ├─ sections/*-form-section.tsx # 도메인 전용 + └─ widgets/pbadms-sprt/* # 공통 재사용 + +entities/sewer-bsc-plan/ # 이미 구현됨 + saveApply / findDetails / submitSupplement … +``` + +**폼 스택 (fnrs-cnsltn과 동일)** + +- `react-hook-form` + `zod` (`zodResolver`, `mode: "onChange"`) +- 섹션 래퍼: `SectionCollapsible` (Accordion 아님) +- UI: `@common/components` (Input, Select, Textarea, Button, Radio, Tabs, Table 등) +- 그리드가 필요한 곳만 `@tanstack/react-table` + `DataTable` +- 데이터: React Query (`useSaveApplySewerBscPlan` 등) — 목데이터는 API 미연동 구간만 `@faker-js/faker` + +**Vercel React Best Practices (적용 요약)** + +- `client-swr-dedup`: React Query로 체크리스트/필수첨부/코드 조회 중복 제거 +- `bundle-dynamic-imports`: 처리시설 Tabs 패널·이전계획 Dialog는 `next/dynamic` 후보 +- `rerender-derived-state`: 단계 연도·합계·진행률은 render 중 파생, 불필요 effect 금지 +- `async-parallel`: 등록 진입 시 코드/체크리스트트리/필수첨부트리를 병렬 조회 + +--- + +## 3. 전체 레이아웃 + +`fnrs-form-page.tsx`와 동일한 **FormProvider + 세로 섹션 스택** 구조를 따르고, +하단 액션바는 **[`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx)** 과 동일 구성(목록·저장·신청완료·삭제·보완완료)이며 **API/라우트만 sewer** 로 바꾼다. +타이틀·브레드크럼은 폼 밖(`PageLayout` / regist·edit·view 셸)에서 처리한다. +(상세 View의 sticky FAB·검토/결재 버튼은 본 등록 문서 범위 밖 — `fnrs-view-page` 대응 후속.) + +### 3-1. 컴포넌트 트리 (fnrs 대응) + +``` +PageLayout (title / breadcrumb) +└─ SewerBscPlanRegistPage | SewerBscPlanEditPage | SewerBscPlanViewPage + └─ SewerBscPlanFormPage | SewerBscPlanViewPage 본문 + FormProvider (등록/수정) / 읽기 섹션 (상세) + └─ + ├─ BasicInfoFormSection // JSP slide_10 + ├─ ContactInfoFormSection // JSP slide_15 (pbadms-sprt) + ├─ PrivacyConsentSection // JSP slide_19 (pbadms-sprt, 등록·수정) + ├─ OutlineFormSection // JSP slide_11 (사업개요, 신규) + ├─ BizCstMatrixFormSection // JSP slide_12 (총사업비 전용, 신규) + ├─ FnrsCstFormSection // JSP slide_13 (재원분담, 신규 ※ FnrsShar 아님) + ├─ TmplntFormSection // JSP slide_14 (처리시설, Tabs로 시설별 패널) + ├─ ChecklistFormSection // JSP slide_16 (pbadms-sprt) + ├─ AttachmentsFormSection // JSP 필수·기타첨부 (pbadms-sprt) + ├─ SplmntDmndFormSection // edit + SL 상태만 (pbadms-sprt) + └─ form footer action bar // fnrs-form-page 279~323 동일 + // [목록] / [저장] / edit시 [신청완료][삭제] / SL시 [보완완료] +``` + +**폼 하단 액션바 버튼 구성** +기준: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx). UI·분기만 동일하고 훅/라우트는 sewer. + +| 순서 | 버튼 | 조건 | sewer 비고 | +| ---- | -------- | ----------------------------------------- | ---------------------------------------------------- | +| 1 | 목록 | 항상 | `router.push("/sewer-bsc-plan")` | +| 2 | 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` / 임시저장(SV) | +| 3 | 신청완료 | `!isSplmntPrgrsStts && isEditMode` | 체크리스트+필수첨부 검증 → `saveApply` / LE | +| 4 | 삭제 | `!isSplmntPrgrsStts && isEditMode` | confirm → `delete` → 목록 | +| 5 | 보완완료 | `isSplmntPrgrsStts` (`prgrsSttsComCd=SL`) | `submitSupplement` (fnrs `onSplmntCmptn` 대응) | + +**fnrs와의 차이 (섹션 치환만, 골격 동일)** + +| fnrs-form-page | sewer-bsc-plan | +| ---------------------------------------------- | ------------------------------------------------------ | +| `BasicInfoFormSection` | 동일 역할 (마스터·연도·개요 필드는 sewer DTO) | +| `ContactInfoFormSection` | 동일 (`fieldPrefix="sewerBscPlanDtl"`) | +| `PrivacyConsentSection` | 동일 | +| `BusinessScaleFormSection` | → `OutlineFormSection` (단계별 인구·보급률) | +| `BusinessCostFormSection` | → `BizCstMatrixFormSection` (단계×원가 계층 전용) | +| `FnrsSharFormSection` | → `FnrsCstFormSection` (국고·지방비·원인자·기타) | +| _(없음)_ | `TmplntFormSection` (체크박스 + Tabs 시설별 매트릭스) | +| `ChecklistFormSection` `docSeComCd="FNRS"` | `docSeComCd="SEWER"` (TBD) | +| `AttachmentsFormSection` | 동일 패턴 | +| `SplmntDmndFormSection` | 동일 (`readOnly={!isSplmntPrgrsStts}`) | +| 폼 하단 액션바 (목록·저장·신청완료·삭제·보완완료) | **동일 구성, API·라우트만 sewer** | + +### 3-2. 본문 ASCII (폼 페이지) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PageLayout: 하수도기본계획 신청/수정 breadcrumb │ +├─────────────────────────────────────────────────────────────────┤ +│ FormProvider │ +│
│ +│ │ +│ ┌─ SectionCollapsible: 기본계획 신청 ─────────────────────┐ │ +│ │ BasicInfoFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 신청자 정보 ────────────────────────────────────────────┐ │ +│ │ ContactInfoFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 개인정보 동의 ──────────────────────────────────────────┐ │ +│ │ PrivacyConsentSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 사업개요 ────────────── [이전기본계획조회] ─────────────┐ │ +│ │ OutlineFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 총사업비 (백만원) ──────────────────────────────────────┐ │ +│ │ BizCstMatrixFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 재원분담 (백만원) ──────────────────────────────────────┐ │ +│ │ FnrsCstFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 처리시설별 하수도정비기본계획 ──────────────────────────┐ │ +│ │ TmplntFormSection │ │ +│ │ ├─ BscFctList Checkbox │ │ +│ │ └─ Tabs (선택 시설별 TabsTrigger / TabsContent) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 체크리스트 ─────────────────────────────────────────────┐ │ +│ │ ChecklistFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 첨부파일 ───────────────────────────────────────────────┐ │ +│ │ AttachmentsFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ ┌─ 보완요구 (edit + SL) ───────────────────────────────────┐ │ +│ │ SplmntDmndFormSection │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ form footer (fnrs-form-page 279~323 동일) ──────────────┐ │ +│ │ [목록] [저장] / edit: [신청완료][삭제] / SL: [보완완료] │ │ +│ │ ※ API·라우트만 sewer — UI 분기는 fnrs와 동일 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│
│ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3-3. 액션바 동작 (`fnrs-form-page` 279~323 동일, API만 sewer) + +구현 위치: `sewer-bsc-plan-form-page.tsx` 하단 +(`fnrs-form-page.tsx`의 `mx-4 flex flex-wrap justify-center gap-3` 블록을 그대로 옮기고 훅·path만 교체) + +| 버튼 | 조건 | 대표 동작 | +| -------- | ---------------------------- | -------------------------------------------------------------------------- | +| 목록 | 항상 | `router.push("/sewer-bsc-plan")` | +| 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` + 임시저장(`SV`) | +| 신청완료 | edit && `!isSplmntPrgrsStts` | 체크리스트 완료 + **필수 첨부 완료** 검증 → `saveApply` + `LE` | +| 삭제 | edit && `!isSplmntPrgrsStts` | confirm → `delete` → 목록 | +| 보완완료 | `isSplmntPrgrsStts` (SL) | `form.handleSubmit` → `useSubmitSupplementSewerBscPlan` | + +**신청완료 사전 검증** (fnrs `handleApplyClick`과 동일 정책) + +1. **체크리스트**: `isFormChecklistComplete(cklstResultList)` — 미완료 시 `"모든 체크리스트 항목을 체크해야 신청이 가능합니다."` +2. **필수 첨부**: `esntlAtchYn === "Y"`인 필수첨부 슬롯마다 `esntlAtchFileList`에 `atchFileGroupId`가 있어야 함 + - Form: `AttachmentsFormSection`의 트리 + 폼 `esntlAtchFileList` + - 미충족 시 예: `"필수 첨부파일을 모두 등록해야 신청이 가능합니다."` + - 헬퍼 후보: `isFormEsntlAtchComplete(requiredTree, esntlAtchFileList)` (`pbadms-sprt`에 추가) + +저장에는 위 검증을 적용하지 않는다 (임시저장 허용). + +저장/제출 버튼을 섹션 헤더(`gTitle2 gRt`)에 두지 않는다 — **폼 하단 액션바 단일 진입점** (§4-J). + +### 3-4. 라우트 + +| 화면 | path | 셸 → FormPage | +| ---- | --------------------------- | ------------------------------------------ | +| 등록 | `/sewer-bsc-plan/regist` | `mode="create"` | +| 수정 | `/sewer-bsc-plan/[id]/edit` | `mode="edit"` + `useSewerBscPlanDetails` | +| 상세 | `/sewer-bsc-plan/[id]` | ViewPage (본 문서 범위 밖, fnrs-view 대응) | + +`docSeComCd` 후보: 백엔드 체크리스트·필수첨부 등록값 확인 후 **`SEWER`** (또는 레거시 `DMBP`) — **TBD, CMS 코드와 맞출 것**. + +--- + +## 4. 섹션별 상세 구현 + +### 4-0. Hidden / 세션 컨텍스트 (폼 루트) + +JSP hidden → React 폼 기본값·세션 스토어로 흡수. + +| JSP | 의미 | React | +| ------------------------------- | --------------- | ------------------------------------------------------- | +| `chgSe` / `chgSeSp` / `chgSeCd` | 변경구분 | `sewerBscPlan.sewerBscPlanSeComCd` + 표시용 코드명 | +| `progrsSttus` | SAVE / PRESENTN | 저장·제출 시 `prgrsSttsComCd`로 매핑 (임시저장 vs 제출) | +| `planSeCd` | 고정 `A` | 필요 시 상수 또는 API 필드 확인 | +| `adstDstrCd` / `rgnCd` | 행정구역 | `sewerBscPlan.admdstCd` | +| `sidoNm` / `gugunNm` | 표시용 | 읽기 전용 표시 (`detDto.sido`/`gugun`) | + +구현: `getCreateDefaultValues(userInfo)`에서 로그인 사용자·소속 지자체 주입. + +--- + +### 4-A. 타이틀·브레드크럼 + +| JSP | React | +| ------------------------ | ------------------------------------------------------ | +| `h3` 기본계획검토 | PageLayout title: **하수도기본계획 신청** (IIPBS 명명) | +| 설치인가 및 제원협의 > … | 메뉴 메타 기반 breadcrumb | + +별도 섹션 컴포넌트 불필요. 라우트 `page.tsx` / layout metadata로 처리. + +--- + +### 4-B. 기본계획변경 신청 (`slide_10`) + +**파일**: `ui/sections/basic-info-form-section.tsx` +**래퍼**: `SectionCollapsible` title=`기본계획 신청` +**필드 접두사**: `sewerBscPlan.*` + `sewerBscPlanDtl.*` + +| 표시명 | JSP | API 매핑 | UI | 필수 | 비고 | +| ---------------- | ----------------- | ------------------------------- | ------------------ | ---- | ---------------------------------- | +| 기본계획명 | `drngMtncPlanNm` | `sewerBscPlan.sewerBscPlanNm` | Input | Y | | +| 계획년도 | `planYr` | `sewerBscPlanDtl.planYr` | Select (현재~2005) | Y | | +| 기준년도 | `crtrYr` | `sewerBscPlanDtl.crtrYr` | Select | Y | | +| 1~4단계 목표연도 | `goalYrStep1~4` | `frlvlGoalYr`~`folvlGoalYr` | Select (현재~+30) | Y | 1단계 변경 시 +5/+10/+15 자동 연동 | +| 계획개요 | `bzCn` | `sewerBscPlanDtl.bizOtlnCn` | Textarea | Y | | +| 구분 | `#chgSeNm` | `sewerBscPlanSeComCd` (+코드명) | 읽기 또는 Select | | 신규/변경 등 | +| 신청 지자체 | `${lclgvrnmntNm}` | `admdstCd` | 읽기 전용 | | | + +**동작** + +- `goalYrStep1` onChange → 2·3·4단계 기본값 세팅 + E/F/G 헤더 연도 span 갱신 (`watch` 파생) +- JSP 상단 `gRt` 저장/제출은 사용하지 않음 → FormPage 하단 액션바 (§3-3, §4-J) + +--- + +### 4-C. 신청자 정보 (`slide_15`) + +**재사용**: `@/widgets/pbadms-sprt`의 `ContactInfoFormSection` +**props**: `fieldPrefix="sewerBscPlanDtl"` (또는 dtl 필드 구조에 맞게) + +| 표시 | JSP | API | +| ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------- | +| 신청자 이름/전화/부서/이메일 | `${UsrNm}` 등 서버렌더 | 로그인 사용자 읽기 전용 → `aplcntNm`, `aplcntMblTelno`, `aplcntOgdpNm`, `aplcntEmlAddr` | +| 설계자 | `dsgnrNm/Telno/Psitn/Email` | `dsgnrNm`, `dsgnrTelno`, `dsgnrOgdpNm`, `dsgnrEmlAddr` | + +신청자 4필드는 세션에서 defaultValues에 채우고 Input `disabled`. + +--- + +### 4-D. 개인정보 동의 (`slide_19`) + +**재사용**: `PrivacyConsentSection` + +| JSP | React | +| ----------------------------- | ------------------------------------------------------------------ | +| 고지 textarea + 보유기간 문구 | 동일 문구 상수/마크다운 | +| `privacyAgree` Y/N | 폼 필드 `privacyAgree: "Y" \| "N"` (API 미전송, 클라이언트 검증만) | + +**검증**: 제출 시 `Y` 필수 (JSP `presentnBtn`과 동일). 저장(임시)은 동의 없이 허용할지 정책 TBD — 기본은 JSP와 같이 **제출만 강제**. + +--- + +### 4-E. 사업개요 (`slide_11`) + +**파일**: `ui/sections/outline-form-section.tsx` (신규, 도메인 전용) +**API**: `sewerBscPlanOtlnList: SewerBscPlanOtlnItem[]` (planStpNo 1~4) + +| 행(구분) | JSP name | Item 필드 | +| ------------ | --------------- | ------------------- | +| 계획인구 | `arrPlanPpltn` | `sewerPlanPpltnCnt` | +| 처리인구 | `arrPrcsPpltn` | `sewerPrcsPpltnCnt` | +| 하수도보급율 | `arrDrngSplyRt` | `sewerPsplyRt` | + +열: 1~4단계 (헤더에 `frlvlGoalYr` 등 표시). + +**버튼**: `이전기본계획조회` → Dialog (`PreviousPlanLookupDialog`) + +- 레거시: `fnbscPlnPopupToggle` + `bscPlnPopup.jsp` +- React: Modal + 조회 API (**TBD**: 이전 계획 조회 엔드포인트; 없으면 1차는 Dialog UI만 + mock) + +숫자 입력: 콤마 제거 후 number 전송 (제출 시 JSP `replaceComma` 대체). + +--- + +### 4-F. 총사업비 (`slide_12`) + +**파일**: `ui/sections/biz-cst-matrix-form-section.tsx` (**전용 컴포넌트** — fnrs `BusinessCostFormSection` 재사용 금지) +**래퍼**: `SectionCollapsible` title=`총사업비` (단위 `백만원`은 헤더 우측) + +**JSP 현실**: 단계(1~4) × 원가 계층(총사업비, 공사비, 처리시설, 하수관로 신설/보수, 펌프장, 배수설비, 기타, 부대경비, 설계/감리/부대비). 일부 readonly·자동합산 JS. + +**신규 API**: `bizCstList: SewerBscPlanBizCstItem[]` = `{ planStpNo, acntgSeCd, cstrnAmt }` + +**구현 방향 (`BizCstMatrixFormSection` 전용)** + +- 행 = `useAcntgSeMngTree` → `buildAcntgCostItems` (fnrs `BusinessCostFormSection`과 동일 출처) +- `costItems` 로드 시 `useEffect`로 `bizCstList`를 `mergeBizCstList(existing, costItems)`로 동기화 +- 열 = `planStpNo` 1~4 (헤더에 목표연도 표시) +- readonly 행(`CostItemDef.readonly=true`)은 children 합산으로 자동계산; Input에 `readOnly` + `bg-muted` +- 변경 시 `setAmt` → `recalculateBizCstMatrix(list, costItems)` → `setValue("bizCstList")` +- 저장 시 섹션 onChange 합산을 신뢰하여 `bizCstList` 그대로 전송 (`toBizCstSaveList` identity) + +**표준사업비 조회** 버튼은 JSP에서 주석 처리됨 → **미구현**. + +--- + +### 4-G. 재원분담 (`slide_13`) + +**주의**: fnrs의 `FnrsSharFormSection`(재원협의 공유구분 트리)과 **다름**. + +| 행 | JSP | `SewerBscPlanFnrsCstItem` | +| ------------ | -------------- | ------------------------- | +| 총사업비 | `arrTotAmnt` | `sumAmt` (또는 파생) | +| 국고 | `arrNlepAmt` | `gvexpAmt` | +| 지방비 | `arrLcepAmt` | `lcexpAmt` | +| 원인자부담금 | `arrCusrAlotm` | `orgprBdchgAmt` | +| 기타 | `arrEtcAmt` | `etcAmt` | + +**파일**: `ui/sections/fnrs-cst-form-section.tsx` (신규) +데이터: `fnrsCstList` 길이 4 (`planStpNo` 1~4). + +검증(선택): 단계별 `gvexp+lcexp+orgpr+etc === sumAmt` (JSP `calculateTotalFunding` 참고). + +--- + +### 4-H. 처리시설별 하수도정비기본계획 (`slide_14`) — 핵심 복잡도 + +**JSP 동작** + +1. `BscFctList` 체크박스로 처리시설 다중 선택 (`arrBscFctCd`) +2. 선택 시 `createFormSection(code, name)`로 **당초 4단계 | 현재 | 변경 4단계** 매트릭스 동적 삽입 + → **React**: 세로 삽입 대신 `@common/components` `Tabs`로 시설별 패널 관리 (§아래 Tabs 규칙) +3. 활성 `formFields`(축소판): 처리구역면적, 하수처리인구, 계획하수량, 시설용량, 분류식 오수/우수, 합류식, 차집, 배수설비, 오수중계/일반/우수 펌프 +4. 시설별 필수첨부 `createFileAttachmentSection` +5. `fetchBasicPlanData`로 당초(이전) 데이터 바인딩 + +**API 매핑** + +| UI | DTO | +| -------------------- | ------------------------------------------------------------- | +| 선택 시설 목록 | `sewerBscPlanTmplntList[].swtrmtFcltCd` (+ `atchFileGroupId`) | +| 단계별 수치 (변경측) | `sewerBscPlanTmplntDtlList[]` → `SewerBscPlanTmplntDtlItem` | +| 임시저장 | `tmplntTmprStrgList` (편집 중 임시, 필요 시) | + +필드 대응 (변경 단계 기준): + +| UI 라벨 | `SewerBscPlanTmplntDtlItem` | +| -------------- | --------------------------- | +| 처리구역면적 | `prcsZoneArea` | +| 하수처리인구 | `swtrmtPpltnCnt` | +| 계획하수량 | `planSwqty` | +| 시설용량 | `fcltCpct` | +| 분류식 오수 | `sprsysDrtwPplLen` | +| 분류식 우수 | `sprsysExlnPplLen` | +| 합류식 | `cbsysPplLen` | +| 차집관로 | `icptgPplLen` | +| 배수설비 | `drgCnt` | +| 오수 중계 펌프 | `drtwRlyPumpCnt` | +| 오수 일반 펌프 | `drtwPumpCnt` | +| 우수 펌프 | `exlnPumpCnt` | + +**구현 구성** + +``` +ui/sections/tmplnt-form-section.tsx + ├─ TmplntFacilityPicker # BscFctList Checkbox 목록 (시설 마스터 API TBD) + └─ Tabs (@common/components Tabs) # 선택 시설별 동적 패널 + ├─ TabsList / TabsTrigger # value=swtrmtFcltCd, 라벨=시설명 + └─ TabsContent (시설 1개당) + ├─ TmplntMatrixTable # 당초|현재|변경 매트릭스 (useFieldArray) + └─ TmplntFacilityAttachments # 시설별 필수첨부 (선택) +``` + +**Tabs 관리 규칙** (`packages/common/components/src/ui/tabs`) + +- `BscFctList` 체크 **선택** → `useFieldArray` append + 해당 시설 `TabsTrigger`/`TabsContent` 추가 → `Tabs` `value`를 신규 시설로 전환 +- 체크 **해제** → confirm 후 remove + 해당 탭 제거 → 남은 탭 중 하나로 `value` 이동 (없으면 Tabs 영역 비표시) +- 선택 시설이 0개면 Tabs UI 숨김; 1개 이상이면 Tabs로 시설별 패널 전환 +- 매트릭스·첨부는 각 `TabsContent` 안에만 렌더 (JSP의 세로 동적 삽입을 탭 패널로 대체) + +**매트릭스 열** + +- **당초 열**: 이전계획 API 조회 결과 읽기 전용 (없으면 빈 칸) +- **현재 열**: 기준년도 시점 값 (API 제공 시) +- **변경 열**: 편집 가능 → 저장 payload의 dtl만 전송 + +시설 마스터 목록 API: 레거시 `BscFctList` 대체 — **TBD** (`swtrmtFclt` 계열). + +`bundle-dynamic-imports`: `TmplntFormSection`(또는 TabsContent 내부 매트릭스)을 `next/dynamic` 후보. + +--- + +### 4-I. 체크리스트 + 첨부 (`slide_16`) + +#### I-1. 체크리스트 + +**재사용**: `ChecklistFormSection` +`docSeComCd=""` +`fieldPrefix`: 마스터에서 `taskSeComCd`를 읽는 경로 (예: `sewerBscPlan`) + +| JSP | React | +| ----------------------- | ------------------------------------------- | +| 업무탭 `ceckEssntlArr` | 코드 `taskSeComCd` 다중 → Tabs | +| 진행률 / 전체 예·아니오 | 위젯 내장 로직 | +| `exmntResult` Y/N | `cklstResultList[].selected` (+ `picRvwCn`) | + +저장: `cklstResultList: CklstResultItem[]` + +#### I-2. 필수 첨부 (탭별) + +**재사용**: `AttachmentsFormSection` +`docSeComCd` 동일, `esntlAtchFileList` + +레거시: 체크리스트 탭마다 `essntlList` + `uploadFile` multipart. +신규: TUS/`attach-service` + `esntlAtchFileId` / `atchFileGroupId` (fnrs와 동일). + +#### I-3. 기타 첨부 + +`AttachmentsFormSection`의 etc 영역 또는 `atchFileGroupId` on `sewerBscPlanDtl`. +JSP `uploadFileETC` multiple → Zip 안내 문구 유지. + +--- + +### 4-J. 저장 / 제출 + +**UI 구성**: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx) 과 **동일**. +마크업·분기(`isSplmntPrgrsStts` / `isEditMode`)·버튼 색상·`disabled={isSubmitting}` 패턴을 그대로 두고, **호출 API·목록 path만 sewer** 로 교체한다. + +```tsx +
+ + {!isSplmntPrgrsStts ? ( + <> + + {isEditMode && ( + <> + + + + )} + + ) : ( + + )} +
+``` + +| 버튼 | fnrs 대응 | sewer API / 동작 | +| -------- | ----------------------------- | ------------------------------------------------------------ | +| 목록 | `router.push("/fnrs-cnsltn")` | `router.push("/sewer-bsc-plan")` | +| 저장 | `saveApply` + 임시저장 | `useSaveApplySewerBscPlan` + `prgrsSttsComCd`=임시저장(`SV`) | +| 신청완료 | `handleApplyClick` → 제출 | 체크리스트+필수첨부 검증 → `saveApply` + `LE` (상태코드 TBD) | +| 삭제 | `handleDelClick` | `useDeleteSewerBscPlan`(또는 동등) → 목록 | +| 보완완료 | `onSplmntCmptn` | `useSubmitSupplementSewerBscPlan` | + +| JSP | 진행값 | URL | React | +| ---- | ---------- | ------------------------- | ---------------------------------------------------------- | +| 저장 | `SAVE` | `insertFnrscDmbp.do` | `saveApply` + 임시저장 코드 | +| 제출 | `PRESENTN` | `insertFnrscDscss.do` (!) | `saveApply` + 제출 상태코드 — **백엔드 상태코드 확인 TBD** | + +페이로드 조립 (`buildApplyPayload`): + +```ts +SewerBscPlanApplySaveRequestDto { + sewerBscPlan, sewerBscPlanDtl, + sewerBscPlanOtlnList, bizCstList, fnrsCstList, + sewerBscPlanTmplntList, tmplntTmprStrgList?, + cklstResultList, esntlAtchFileList, + splmntDmndList?, rvwOpnnList? // 등록 화면에서는 보통 빈 배열 +} +``` + +성공 후: `invalidateQueries(sewerBscPlanBaseKey)` → `router.push('/sewer-bsc-plan')` (신청완료는 상세로 보낼 수 있음 — fnrs와 동일 정책). + +권한: JSP `AUTH3` → 기존 AS 권한 훅/역할로 버튼 `disabled`/`hidden`. + +--- + +## 5. API / DTO 연결 + +Base: `/as-service/api/v1/sewer-bsc-plans` +엔티티: `@/entities/sewer-bsc-plan` + +| 화면 동작 | Hook / Service | 비고 | +| -------------- | ---------------------------------------- | ---------------- | +| 등록 저장·제출 | `useSaveApplySewerBscPlan` → `saveApply` | 본 문서 핵심 | +| 수정 로드 | `useSewerBscPlanDetails(docNo, chgCycl)` | 편집 시 | +| 마스터만 | `useSewerBscPlan` | 목록 행 클릭 등 | +| 보완 제출 | `useSubmitSupplementSewerBscPlan` | edit + `prgrsSttsComCd=SL` 시 폼 하단 [보완완료] | +| 기술검토 | `useTechReviewSewerBscPlan` | 등록 JSP 범위 밖 | + +등록 JSP에 없는 섹션(보완요구·검토의견)은 **상세/검토 화면**에서 `SplmntDmndFormSection` / 검토의견 섹션으로 후속 구현. + +--- + +## 6. 권장 파일 배치 + +``` +apps/as/ +├── app/(with-base-layout)/sewer-bsc-plan/ +│ └── regist/page.tsx +├── src/pages/sewer-bsc-plan/ +│ ├── lib/ +│ │ ├── sewer-bsc-plan-regist-schema.ts # zod +│ │ └── form-data-mapper.ts # Det ↔ Form, Apply payload +│ ├── model/ +│ │ └── (필요 시 table columns) +│ └── ui/ +│ ├── sewer-bsc-plan-regist-page.tsx +│ ├── sewer-bsc-plan-form-page.tsx +│ ├── components/ +│ │ └── previous-plan-lookup-dialog.tsx +│ └── sections/ +│ ├── basic-info-form-section.tsx +│ ├── outline-form-section.tsx # 사업개요 +│ ├── biz-cst-matrix-form-section.tsx # 총사업비 (전용) +│ ├── fnrs-cst-form-section.tsx # 재원분담 +│ └── tmplnt-form-section.tsx # 처리시설 (Checkbox + Tabs) +└── docs/sewer-bsc-plan/ + └── dmbp-regist-migration.md # 본 문서 +``` + +공통 재사용 (신규 작성 최소화): + +- `ContactInfoFormSection`, `PrivacyConsentSection` +- `ChecklistFormSection`, `AttachmentsFormSection` +- `SectionCollapsible` +- `Tabs` / `TabsList` / `TabsTrigger` / `TabsContent` (`@common/components` — 처리시설 패널) + +도메인 전용 (재사용 금지): + +- `BizCstMatrixFormSection` — 총사업비 (fnrs `BusinessCostFormSection` 미사용) +- 폼 하단 액션바 — `fnrs-form-page` 279~323 복제, API만 sewer + +--- + +## 7. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 (등록) + CALL 로그인사용자조회 + CALL 코드조회(SEWERBSCPLANSE, SEWER, …) // 병렬 + CALL 체크리스트트리(docSeComCd) // 병렬 + CALL 필수첨부트리(docSeComCd) // 병렬 + CALL 처리시설마스터목록 // TBD (BscFctList) + DISPLAY 폼 defaultValues + + ON 1단계목표연도 CHANGE DO + SET 2·3·4단계 연도 (+5/+10/+15) + DISPLAY E/F/G/H 헤더 연도 + END ON + + ON 처리시설 체크 DO + ADD tmplntList 항목 + ADD TabsTrigger / TabsContent (시설 패널) + SET Tabs value = 신규 시설 + CALL 이전기본계획수치(시설코드) // 당초열, TBD + END ON + + ON 처리시설 체크해제 DO + CONFIRM 제거 + REMOVE tmplntList + 해당 Tabs 패널 + SET Tabs value = 남은 시설 (없으면 Tabs 숨김) + END ON + + ON [이전기본계획조회] CLICK DO + OPEN PreviousPlanLookupDialog + END ON + + ON [저장] CLICK DO + VALIDATE zod (동의 제외 가능) + CALL saveApply(payload, 임시저장 상태) + NAVIGATE 목록 + END ON + + ON [신청완료] CLICK DO + VALIDATE zod + privacyAgree=Y + 체크리스트 완료 + 필수첨부(esntlAtchYn=Y) 완료 + CONFIRM 신청 확인 + CALL saveApply(payload, 제출 상태 LE) + NAVIGATE 상세 + END ON + +END +``` + +--- + +## 8. JSP ↔ 신규 갭·리스크 + +| # | 이슈 | 대응 | +| --- | ---------------------------------------------------- | ------------------------------------------------- | +| 1 | JSP 제출 URL이 `dscss`로 분기 (오타/구프로세스 가능) | 신규는 `saveApply` 단일 진입으로 통일 | +| 2 | 총사업비 UI 계층 ≠ `BizCstItem` | `BizCstMatrixFormSection` 전용 + `acntgSeCd` 매핑 테이블 | +| 3 | 재원분담 ≠ fnrs shar | 전용 `fnrs-cst-form-section` | +| 4 | 처리시설 매트릭스(당초/현재/변경) | Checkbox 선택 → Tabs 패널; 저장은 변경 dtl 중심; 당초는 조회 전용 | +| 5 | 시설·이전계획 API | TBD — 미정 시 체크박스 목록 mock(`faker`)만 | +| 6 | `docSeComCd` 값 | CMS/체크리스트 마스터와 합의 | +| 7 | 레거시 multipart vs TUS | fnrs 첨부 패턴 준수 | +| 8 | 주석 처리된 상세 `formFields` (자연발생인구 등) | **축소된 formFields만** 이관; 주석 블록 복원 금지 | + +--- + +## 9. 구현 순서 (권장) + +1. 라우트 + `FormPage` 골격 + zod 스키마(마스터·dtl·동의) + 폼 하단 액션바(§4-J) +2. B·C·D 섹션 (공통 위젯 최대 활용) +3. E 사업개요 + G 재원분담 (4열 매트릭스 패턴 공유) +4. F 총사업비 (`BizCstMatrixFormSection` 전용) +5. I 체크리스트·첨부 +6. H 처리시설 (`BscFctList` Checkbox + `@common/components` Tabs — 가장 복잡) +7. 저장/신청완료/삭제/보완완료 API 연동 (§4-J) +8. 이전계획 Dialog (API 확정 후) + +--- + +## 10. 체크리스트 (jsp-migration-as) + +- [ ] contents만 이관 (include 제외) +- [ ] vercel-react-best-practices 적용 (Query 병렬·파생상태·dynamic) +- [ ] `@common/components` + 필요 시 DataTable +- [ ] API·모델은 `@/entities/sewer-bsc-plan` 우선 +- [ ] 목데이터는 `@faker-js/faker`만 (시설 마스터 등 TBD 구간) +- [ ] 로딩·빈·에러 상태 +- [ ] `router.push`로 목록 복귀 + +--- + +## 11. 후속 문서 + +- 목록(`dmbpList.jsp`) / 상세(`dmbpView.jsp`) / 수정(`dmbpModify.jsp`)은 본 문서와 동일 섹션 체계로 별도 확장 +- 화면설계서 SPEC 형식이 필요하면 `docs/sewer-bsc-plan/SPEC.md`로 분리 (영역 A~J 표 + API 표) diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/spec/approval-line-management/SPEC.md similarity index 100% rename from apps/as/docs/approval-line-management/SPEC.md rename to apps/as/docs/spec/approval-line-management/SPEC.md diff --git a/apps/as/docs/checklist-management/SPEC.md b/apps/as/docs/spec/checklist-management/SPEC.md similarity index 100% rename from apps/as/docs/checklist-management/SPEC.md rename to apps/as/docs/spec/checklist-management/SPEC.md diff --git a/apps/as/docs/code-management/SPEC.md b/apps/as/docs/spec/code-management/SPEC.md similarity index 100% rename from apps/as/docs/code-management/SPEC.md rename to apps/as/docs/spec/code-management/SPEC.md diff --git a/apps/as/docs/spec/dashboard/SPEC.md b/apps/as/docs/spec/dashboard/SPEC.md new file mode 100644 index 000000000..3ce6f9800 --- /dev/null +++ b/apps/as/docs/spec/dashboard/SPEC.md @@ -0,0 +1,498 @@ +# 화면설계서: 전국 행정지원 지도 (대시보드 메인) + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 1.2 | +| 프로세스명 | UI설계 | +| 화면명 | 전국 행정지원 지도 (대시보드 메인) | +| 화면 ID | TBD | +| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 | +| 메뉴 경로 | 메인 | + +--- + +## 2. 화면 목적 + +로그인 사용자의 **기관 권한·당해 연도** 기준으로 문서별 진행상태 통계와 행정지원 알림을 한 화면에서 조회한다. 좌측 지도에서 전국 행정지원 현황을 보고, 우측에서 진행상황 카드·알림이력·공지사항을 확인한 뒤 알림 [처리하기]로 연계 업무 화면으로 이동한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +브레드크럼·페이지 타이틀은 스크린샷에 없음. 홈(`/`) 메인 콘텐츠로 구성한다. + +``` +┌────────────────────────────────┬─────────────────────────────────────────────────┐ +│ [영역 A] 전국 행정지원 지도 │ [영역 B] 문서별 진행상황 카드 (2×2) │ +│ 타이틀: 전국 행정지원 지도 │ ┌────────────────┐ ┌────────────────┐ │ +│ │ │ 재원협의 │ │ 설치인가 │ │ +│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │ +│ 마커: 서울 강남구, │ ├────────────────┤ ├────────────────┤ │ +│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │ +│ 부산 해운대구 │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │ +│ │ └────────────────┘ └────────────────┘ │ +│ ├─────────────────────────────────────────────────┤ +│ │ [영역 C] 알림이력 │ +│ │ 알림이력 업데이트 HH:mm [전체보기] │ +│ │ [오늘 N건] [지연 N건] [완료대기 N건] │ +│ │ [뱃지] [일자] 제목 [처리하기] │ +│ │ … (최대 5건) │ +│ ├─────────────────────────────────────────────────┤ +│ │ [영역 D] 공지사항 [더보기] │ +│ │ ┌────┬────────┬────────┬────────┬──────┐ │ +│ │ │번호│ 제목 │ 작성자 │ 작성일 │조회수│ │ +│ │ └────┴────────┴────────┴────────┴──────┘ │ +└────────────────────────────────┴─────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + // searchYear 미전달 → 서버가 당해 연도로 고정. 알림 목록은 최신 5건 + CALL getMainDashboard(DashboardMainSearchDto) // Authorization은 client 인터셉터 + DISPLAY 진행상황 카드 (영역 B) // docPrgrsSttsList, TOTAL 행 제외 + DISPLAY 알림 통계 칩 · 알림 목록 (영역 C) // notiStats, notiList + + CALL getUserPostList(UserPostSearchParams) // board-service, AS_DASHBOARD_BBS_ID + DISPLAY 공지사항 그리드 (영역 D) + + CALL 지도 마커/음영 조회(…) // TBD — 대시보드 API에 없음 + DISPLAY 전국 지도 (영역 A) + + ON 영역 C [전체보기] 버튼 CLICK DO + NAVIGATE /nti-hstry + END ON + + ON 영역 C [처리하기] 버튼 CLICK DO + CALL readNoti(notiId) // entities/noti + NAVIGATE notiUrl // 비어 있으면 이동하지 않음 + CALL getMainDashboard(…) // → 영역 C 갱신 (invalidate) + END ON + + ON 영역 D [더보기] 버튼 CLICK DO + NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID} // as 앱 공지 목록 라우트 미구현. st/cm 관례 + END ON + + ON 영역 D 행 CLICK DO + NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo} + END ON + + ON 영역 B 카드 CLICK DO + NAVIGATE 해당 문서 목록 // §5-3 + END ON + + ON 영역 A 마커 CLICK DO + // TBD — 팝업/필터/이동 여부 미확정 + END ON + +END +``` + +--- + +## 4. 영역 A — 전국 행정지원 지도 + +스크린샷 타이틀: **전국 행정지원 지도**. + +`getMainDashboard` 응답에 지도·좌표·행정구역 음영 데이터는 **없다**. 마커·음영 소스 API는 **TBD**. + +### 4-1. 표시 요소 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ----------- | --------- | ---- | ------ | ----------------- | ----------------------------------------------------------------- | +| 섹션 타이틀 | Text | — | — | — | 고정 문구 `전국 행정지원 지도` | +| 전국 지도 | Map | — | — | TBD | 시도·시군구 경계. 북·동부 음영(청) / 남서부 비음영(백) — 의미 TBD | +| 마커 라벨 | Marker | — | — | TBD | 스크린샷 샘플: `서울 강남구`, `대전 유성구`, `부산 해운대구` | + +### 4-2. 상호작용 + +| 동작 | 설명 | +| ---------- | ----------------------------------------- | +| 마커 클릭 | TBD (상세 팝업 / 해당 지자체 필터 / 이동) | +| 지도 줌·팬 | TBD (스크린샷에 컨트롤 없음) | + +`entities/adstdstr`는 시도·구군 **콤보**만 제공하며 좌표를 주지 않는다. 지도 라이브러리도 as 앱에 없음 — 선정 TBD. + +--- + +## 5. 영역 B — 문서별 진행상황 카드 + +`DashboardMainResponseDto.docPrgrsSttsList`를 **`docSeComCd`로 매핑**해 4장 카드를 그린다. 배열 순서에 의존하지 않는다. + +스크린샷 배치(좌→우, 상→하): + +| 화면 카드 타이틀 | `docSeComCd` | API `docSeNm` | +| ----------------- | ------------ | -------------- | +| 재원협의 진행상황 | `FNRS` | 재원협의 | +| 설치인가 진행상황 | `INSTL` | 설치인가 | +| 설계자문 진행상황 | `CNFM` | 설계자문 | +| 기본계획 진행상황 | `SEWER` | 하수도기본계획 | + +서버 SQL `ORDER BY`는 `INSTL → FNRS → SEWER → CNFM` 이며, `GROUP BY ROLLUP`으로 **`docSeComCd = TOTAL`** 합계 행이 추가된다. 카드에는 TOTAL 행을 **표시하지 않는다**. + +카드 타이틀은 스크린샷 문구(`… 진행상황`)를 쓴다. `SEWER`의 API명 `하수도기본계획`은 화면에서 `기본계획`으로 축약한다. + +### 5-1. 카드 내부 필드 + +각 카드 공통 구조. `totCnt`는 화면에 없음. + +| 표시명 | Field | 비고 | +| ------ | ---------- | ------------------ | +| 신청 | `reqCnt` | 파란 아이콘 + 건수 | +| 진행 | `prgrsCnt` | 초록 아이콘 + 건수 | +| 완료 | `cmptnCnt` | 주황 아이콘 + 건수 | + +상태 그룹(서버): 임시저장(`SV`/`CV`) 제외. 문서별 `prgrs_stts_com_cd` → `REQ`/`PRG`/`CMP`. + +### 5-2. 검색 파라미터 (화면 미표시) + +영역 B에 연도·필터 UI는 없다. 서버 기본값을 따른다. + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ------ | --------- | ---- | --------- | ----------------- | --------------------------------------------------------------------- | +| (없음) | — | — | 당해 연도 | `searchYear` | 미전달 시 서버 `LocalDate.now().getYear()`. 연도 셀렉터 추가 여부 TBD | + +`docSeComCd` 쿼리는 **알림 목록 필터**에만 쓰이며, 진행상황 SQL에는 전달되지 않는다. 카드 4종은 항상 전원 집계한다. + +### 5-3. 상호작용 + +카드 클릭 시 **해당 문서 목록 페이지**로 이동한다. `docSeComCd` → 라우트 고정 매핑. + +| 동작 | `docSeComCd` | 이동 경로 | +| --------- | ------------ | ----------------- | +| 카드 클릭 | `FNRS` | `/fnrs-cnsltn` | +| 카드 클릭 | `INSTL` | `/instl-sncn` | +| 카드 클릭 | `CNFM` | `/design-cnstn` | +| 카드 클릭 | `SEWER` | `/sewer-bsc-plan` | + +--- + +## 6. 영역 C — 알림이력 + +서버가 cms-service 알림을 대시보드 응답에 합친다. 목록은 **page=0, size=5** (최신 5건). 페이지네이션 없음. + +### 6-1. 헤더 + +| 표시명 | Field / 소스 | 비고 | +| ------------- | ------------ | --------------------------------------------------------------------------------------- | +| 섹션 타이틀 | — | 고정 문구 `알림이력` | +| 업데이트 시각 | (client) | 스크린샷 `업데이트 10:32`. API 필드 없음. `getMainDashboard` 마지막 fetch 시각(`HH:mm`) | +| 전체보기 | Button | `/nti-hstry`로 이동 | + +### 6-2. 통계 칩 + +`NotiAsStatResponseDto`. + +| 표시명 | Field | 비고 | +| ------------ | -------------- | -------------------------------------- | +| 오늘 N건 | `todayCount` | 당일 00:00 이후 수신 | +| 지연 N건 | `delayCount` | `notReadYn=N` 이고 수신 후 3일 이상 | +| 완료대기 N건 | `pendingCount` | `notReadYn=Y` 이고 `notiPrcsCmptnYn=N` | + +### 6-3. 목록 행 + +| 표시명 | Field | 비고 | +| ------ | --------- | --------------------------------------------- | +| 일자 | `regDt` | 표시 `YYYY-MM-DD`. 응답 `yyyy-MM-dd HH:mm:ss` | +| 제목 | `notiTtl` | 알림 제목 | +| 처리 | (action) | 스크린샷 라벨 `처리하기`. 클릭 §6-4 | + +응답에 있으나 이 목록에 **표시하지 않음**: + +| Field | 비고 | +| ----------------- | ----------------------------------------- | +| `notiId` | 행 키 · `readNoti` 파라미터 | +| `recvUnqId` | 수신 회원 UUID (서버 로그인 사용자 고정) | +| `senderUnqId` | 발신 회원 UUID | +| `notiType` | `ADSS` | +| `notiCn` | 본문 | +| `notiUrl` | 처리 후 이동 URL. 컬럼 미표시 | +| `notiLnkgUnqNo` | 알림연결고유번호 | +| `docSeComCd` | 문서구분 코드 | +| `docSeComNm` | 문서구분명 (알림이력 화면에서는 연계업무) | +| `notReadYn` | 뱃지 판정 TBD에 사용 가능 | +| `notiIdntyYn` | 확인 여부 | +| `notiIdntyDt` | 확인 일시 | +| `notiPrcsCmptnYn` | 처리 완료 여부 | +| `notiPrcsCmptnDt` | 처리 완료 일시 | + +### 6-4. 행 상호작용 + +| 동작 | 설명 | +| --------------- | ----------------------------------------------------------------------------------------------------------------- | +| [전체보기] 클릭 | `/nti-hstry` (알림이력 목록)로 이동 | +| [처리하기] 클릭 | `readNoti(notiId)` 후 `notiUrl`로 이동. URL이 비면 이동하지 않음(읽음 처리는 수행). `dashboardBaseKey` invalidate | + +`DashboardController`에 읽음 API는 없다. **`entities/noti`의 `readNoti`** 를 쓴다. 알림이력 화면과 동일 (`PUT /cms-service/api/v1/noti/{notiId}/read`). + +### 6-5. 검색 파라미터 (화면 미표시) + +서버 `toNotiSearchDto()` 기본: `notiType = ADSS`. 화면 필터 없음. + +| DashboardMainSearchDto | 화면 | 비고 | +| ---------------------- | ---- | --------------------------------- | +| `notiType` | 없음 | 서버 기본 `ADSS` | +| `notReadYn` | 없음 | 통계 칩 클릭 필터 시 후보 (TBD) | +| `startDt` / `endDt` | 없음 | 대시보드 기본 미전달 | +| `docSeComCd` | 없음 | 알림만 필터. 진행상황 카드와 무관 | + +--- + +## 7. 영역 D — 공지사항 + +`getMainDashboard`에 공지 필드는 없다. **board-service** `getUserPostList`로 별도 조회한다. + +- Base: `GET /board-service/api/v1/posts/user/{AS_DASHBOARD_BBS_ID}` +- `AS_DASHBOARD_BBS_ID` = `NEXT_PUBLIC_AS_DASHBOARD_BBS_ID` (로컬 예: `7`) +- 클라이언트: `@/entities/board` — `postApi.getUserPostList` / `useUserPostList` + +페이지네이션 컨트롤은 없다. 요약 목록만 표시한다 (`page=0`, `size=3`). + +### 7-1. 헤더·버튼 + +| 버튼명 | 동작 | +| ------ | ------------------------------------------------------------------------------------------- | +| 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` 로 이동. as 앱에 공지 목록 라우트는 없음(st/cm 관례) | + +### 7-2. 그리드 컬럼 + +스크린샷 헤더 순서. `UserPost`. + +| 표시명 | Field | 비고 | +| ------ | --------- | ---------------------------------------------------- | +| 번호 | `rowNo` | `upndFixYn` = `Y` 이면 `[공지]` Badge, 그 외 `rowNo` | +| 제목 | `pstTtl` | truncate | +| 작성자 | `wrtrNm` | | +| 작성일 | `regDt` | 표시 `YYYY-MM-DD` (응답 datetime) | +| 조회수 | `inqNocs` | 숫자 | + +조회 파라미터 (`UserPostSearchParams`): + +| 필드 | 값 | 비고 | +| ------------- | ----------- | ------------------------------------------ | +| `page` | `0` | Spring 0-based | +| `size` | `3` | 대시보드 요약. 스크린샷 3행 | +| `keywordType` | `bbsTypeCd` | st 대시보드 공지 패널과 동일 | +| `keyword` | `BBSNOTICE` | 게시판 유형. path의 bbsId와 함께 공지 한정 | + +응답에 있으나 그리드에 **표시하지 않음**: `bbsId`, `pstNo`(행 키·상세 이동), `pstCn`, `upndFixYn`(번호 열 판정만), `rgtrNm`, `atflCd` 등. + +### 7-3. 행 상호작용 + +| 동작 | 설명 | +| ------- | ------------------------------------------------------------------------------ | +| 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` (as 앱 공지 상세 라우트 미구현) | + +--- + +## 8. API/DTO 명세 + +### 8-1. 공통 + +| 항목 | 값 | +| --------------- | ---------------------------------------------------------------------------------------------- | +| Base prefix | `/as-service/api/v1/dashboard` (`ENDPOINT.AS_SERVICE.DASHBOARD`) | +| 인증 | `Authorization` 헤더. 프론트는 `client` 인터셉터. 로그인 기관 권한은 서버가 사용자 캐시로 적용 | +| 클라이언트 모듈 | `@/entities/dashboard` — `dashboardService` / `useGetMainDashboard` | +| 알림 읽음 | `@/entities/noti` — `notiService.readNoti` / `useReadNoti` | +| 공지사항 | `@/entities/board` — `postApi.getUserPostList` / `useUserPostList` | + +영역 A(지도)는 본 컨트롤러·board-service 범위 밖 → **TBD**. + +### 8-2. 대시보드 메인 조회 — `getMainDashboard` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------------------------------------------- | ------ | ------------- | ------------------------------- | -------------------------- | ----------------------------------------------------------- | +| 대시보드 메인 통합진행상태 및 알림 정보 조회 | GET | `{BASE}/main` | Query: `DashboardMainSearchDto` | `DashboardMainResponseDto` | `dashboardService.getMainDashboard` / `useGetMainDashboard` | + +**Request (`DashboardMainSearchDto`)** + +| 필드 | 필수 | 설명 | +| ------------ | ---- | ---------------------------------------------------------------------- | +| `searchYear` | N | 검색 연도. 미전달 시 서버 당해 연도. 영역 B 집계 기간 | +| `notiType` | N | 알림 종류. 미전달 시 서버 `ADSS` | +| `notReadYn` | N | 알림 확인 여부 `Y`/`N`. 본 화면 미사용 | +| `startDt` | N | 알림 시작일 `YYYY-MM-DD`. 본 화면 미사용 | +| `endDt` | N | 알림 종료일 `YYYY-MM-DD`. 본 화면 미사용 | +| `docSeComCd` | N | 알림 문서구분 (`INSTL`/`FNRS`/`SEWER`/`CNFM` 등). 진행상황 집계와 무관 | + +`Authorization`은 service 시그니처에 넣지 않는다. + +**Response (`DashboardMainResponseDto`)** + +| 필드 | 타입 | 설명 | +| ------------------ | --------------------------- | --------------------------- | +| `docPrgrsSttsList` | `DocPrgrsSttsResponseDto[]` | 문서별 진행상태 + TOTAL 행 | +| `notiList` | `NotiAsUserResponseDto[]` | 최신 최대 5건 | +| `notiStats` | `NotiAsStatResponseDto` | 오늘 / 지연 / 완료대기 건수 | + +**`DocPrgrsSttsResponseDto`** + +| 필드 | 타입 | 설명 | +| ------------ | ------ | --------------------------------------------- | +| `docSeNm` | string | 문서 구분 명칭 | +| `docSeComCd` | string | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `TOTAL` | +| `reqCnt` | number | 신청 건수 | +| `prgrsCnt` | number | 진행 건수 | +| `cmptnCnt` | number | 완료 건수 | +| `totCnt` | number | 총 건수 (화면 미표시) | + +**`NotiAsStatResponseDto`** + +| 필드 | 타입 | 설명 | +| -------------- | ------ | --------- | +| `todayCount` | number | 오늘 알림 | +| `delayCount` | number | 지연 알림 | +| `pendingCount` | number | 완료대기 | + +**`NotiAsUserResponseDto`** — 필드 상세는 `docs/spec/nti-hstry` §6-2와 동일 (`notiId`, `notiTtl`, `regDt`, `notiUrl`, `notReadYn`, `notiPrcsCmptnYn` 등). + +### 8-3. 알림 읽음 처리 — `readNoti` + +대시보드 컨트롤러가 아니라 cms-service. 영역 C [처리하기]에서 호출. + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------------- | ------ | ---------------------------------------- | ----------------------- | ---------------- | -------------------------------------- | +| 알림 읽음 처리 | PUT | `/cms-service/api/v1/noti/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` | + +Body 없음. 성공 시 `dashboardBaseKey`(및 필요 시 `notiBaseKey`) invalidate. + +### 8-4. 전국 행정지원 지도 — TBD + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ------------------- | ------ | --- | ----------- | ------------ | --- | +| 지도 마커·음영 조회 | TBD | TBD | TBD | TBD | TBD | + +### 8-5. 공지사항 — `getUserPostList` + +board-service. bbsId는 환경변수 `AS_DASHBOARD_BBS_ID`로 path에 고정한다. + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ----------------------- | ------ | ------------------------------------------ | ----------------------------- | ------------------------ | --------------------------------------------- | +| 대시보드 공지 요약 목록 | GET | `/board-service/api/v1/posts/user/{bbsId}` | Query: `UserPostSearchParams` | `PageResponse` | `postApi.getUserPostList` / `useUserPostList` | + +**Request (`UserPostSearchParams`)** — 본 화면 고정값 + +| 필드 | 필수 | 설명 | +| ------------- | ---- | -------------- | +| `page` | N | `0` | +| `size` | N | `3` | +| `keywordType` | N | `bbsTypeCd` | +| `keyword` | N | `BBSNOTICE` | +| `popupYn` 등 | N | 본 화면 미사용 | + +**Response (`UserPost`) — 화면 매핑 필드** + +| 필드 | 타입 | 설명 | +| ----------- | ------ | --------------------------- | +| `rowNo` | number | 번호. 고정글이 아니면 표시 | +| `pstNo` | number | 게시 번호. 상세 이동 키 | +| `bbsId` | number | 게시판 ID | +| `pstTtl` | string | 제목 | +| `wrtrNm` | string | 작성자 | +| `regDt` | string | 작성일 | +| `inqNocs` | number | 조회수 | +| `upndFixYn` | string | 상단고정 `Y`/`N` → `[공지]` | + +--- + +## 9. UI 구현 가이드 + +### 9-1. 기술 스택·원칙 + +- 라우트: 기존 홈 `app/(main)/(with-base-layout)/page.tsx` → `pages/home` (현재 `MainPage` 플레이스홀더) +- UI: `@common/components` — Card, Badge, Button, Table(공지). 지도 라이브러리 TBD +- 데이터: React Query `useGetMainDashboard()`. 처리하기는 `useReadNoti()`. 공지는 `useUserPostList()` +- 진행상황 카드: `docSeComCd`로 4슬롯 매핑. `TOTAL` 제외 +- 알림 목록: 테이블이 아닌 행 리스트(뱃지 + 일자 + 제목 + 버튼) +- 공지: `@tanstack/react-table` + `useDataTable`, 페이지네이션 없음 +- 검색 조건 URL 동기화: 본 화면 필터 없음. `searchYear` UI가 생기면 그때 검토 + +### 9-2. 모듈·파일 배치 + +| 구분 | 경로 | +| ------------- | ------------------------------------------------------------ | +| 라우트 | `app/(main)/(with-base-layout)/page.tsx` | +| 페이지 | `pages/dashboard/ui/dashboard-page.tsx` | +| 지도 | `pages/dashboard/ui/national-as-map.tsx` (API TBD, Skeleton) | +| 진행상황 카드 | `pages/dashboard/ui/doc-prgrs-stts-cards.tsx` | +| 알림이력 | `pages/dashboard/ui/dashboard-noti-list.tsx` | +| 공지사항 | `pages/dashboard/ui/dashboard-notice-table.tsx` | +| 공지 컬럼 | `pages/dashboard/model/dashboard-notice-table-columns.tsx` | +| Entity API | `entities/dashboard/api/dashboard-service.ts` | +| Entity Hook | `entities/dashboard/api/queries.ts` → `useGetMainDashboard` | +| Entity Model | `entities/dashboard/model/dashboard-interface.ts` | +| 알림 읽음 | `entities/noti` → `useReadNoti` | +| 공지 Entity | `entities/board` → `useUserPostList` | + +Entity(`getMainDashboard`, `getUserPostList`) 구현됨. 영역 B·C·D 화면 구현. 영역 A는 Skeleton. + +### 9-3. 영역별 UI 매핑 + +| 영역 | 컴포넌트 | 데이터 | +| ---- | ------------------------ | ---------------------------------------------------------------------------- | +| A | `national-as-map` | Skeleton (지도 API TBD) | +| B | `doc-prgrs-stts-cards` | `useGetMainDashboard` → `docPrgrsSttsList` (`TOTAL` 제외, 코드 매핑) | +| C | `dashboard-noti-list` | 동 훅 → `notiStats`, `notiList`. 처리 → `useReadNoti`. 전체보기 `/nti-hstry` | +| D | `dashboard-notice-table` | `useUserPostList` → `content[]`. [더보기]·행 클릭 → `/cms/bbsnotice/...` | + +### 9-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| -------------- | -------------------------------------------------------------------------------------- | +| 다이얼로그 | 없음 | +| 본 화면 라우트 | `/` (홈) | +| 알림 처리 이동 | [처리하기] → `readNoti` 후 `notiUrl` (알림이력과 동일, SEWER URL은 상세 세그먼트 제거) | +| 알림 전체보기 | `/nti-hstry` | +| 공지 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` (as 앱 공지 라우트 미구현, st/cm 관례) | +| 공지 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` | +| 진행상황 카드 | §5-3 목록 라우트 (`/fnrs-cnsltn`, `/instl-sncn`, `/design-cnstn`, `/sewer-bsc-plan`) | + +### 9-5. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 (홈) + CALL getMainDashboard({}) + DISPLAY 영역 B // docPrgrsSttsList — FNRS/INSTL/CNFM/SEWER 슬롯 + DISPLAY 영역 C // notiStats + notiList(≤5) + + CALL getUserPostList({ page: 0, size: 3, keywordType: "bbsTypeCd", keyword: "BBSNOTICE" }) + DISPLAY 영역 D + + CALL 지도 데이터 // TBD → 영역 A + + ON 영역 C [전체보기] CLICK DO + NAVIGATE /nti-hstry + END ON + + ON 영역 C [처리하기] CLICK DO + CALL readNoti(notiId) + IF notiUrl THEN + NAVIGATE notiUrl + END IF + CALL getMainDashboard // invalidate dashboardBaseKey + END ON + + ON 영역 B 카드 CLICK DO + NAVIGATE 문서 목록 // §5-3 + END ON + + ON 영역 D [더보기] CLICK DO + NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID} + END ON + + ON 영역 D 행 CLICK DO + NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo} + END ON + +END +``` diff --git a/apps/as/docs/essential-attachment-file-management/SPEC.md b/apps/as/docs/spec/essential-attachment-file-management/SPEC.md similarity index 100% rename from apps/as/docs/essential-attachment-file-management/SPEC.md rename to apps/as/docs/spec/essential-attachment-file-management/SPEC.md diff --git a/apps/as/docs/spec/my-adss-doc/SPEC.md b/apps/as/docs/spec/my-adss-doc/SPEC.md new file mode 100644 index 000000000..37ae2937d --- /dev/null +++ b/apps/as/docs/spec/my-adss-doc/SPEC.md @@ -0,0 +1,285 @@ +# 화면설계서: 내 행정지원 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 1.0 | +| 프로세스명 | UI설계 | +| 화면명 | 내 행정지원 | +| 화면 ID | AS_SC0204011 | +| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 | +| 메뉴 경로 | 메인 > 행정지원서비스 > 내 행정지원 | + +--- + +## 2. 화면 목적 + +로그인한 사용자가 **본인이 신청한 행정지원 문서**를 기간·구분·진행상태로 조회한다. 목록에서 행을 선택하면 해당 문서 상세 화면으로 이동한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 브레드크럼: 메인 > 행정지원서비스 > 내 행정지원 │ +│ 페이지 타이틀: 내 행정지원 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ 기간 [시작일] ~ [종료일] 구분 ▼ 진행상태 ▼ [검색] │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 내 신청 목록 │ +│ 타이틀: 내 신청 목록 N개 │ +│ ┌────┬──────┬────────┬──────────────┬────────────┬────────┬──────────────┐ │ +│ │ No │ 구분 │ 신청일 │ 진행상태 │ 완료(예정)일│ 담당자 │ 담당자연락처 │ │ +│ ├────┼──────┼────────┼──────────────┼────────────┼────────┼──────────────┤ │ +│ │ … │ … │ … │ prgrsSttsComNm│ … │ … │ … │ │ +│ └────┴──────┴────────┴──────────────┴────────────┴────────┴──────────────┘ │ +│ [페이지네이션] │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + // 기간 기본값: 오늘 기준 최근 1개월 (startDate = today-1M, endDate = today) + CALL findMyElaperDocList(ElaprMyDocSearchDto + pageable) + DISPLAY 내 신청 목록 그리드 (영역 B) + + ON [검색] 버튼 CLICK DO + CALL findMyElaperDocList(영역 A 조건 + page=0) + DISPLAY 내 신청 목록 그리드 (영역 B) + END ON + + ON 페이지네이션 CHANGE DO + CALL findMyElaperDocList(영역 A 조건 + page/size) + DISPLAY 내 신청 목록 그리드 (영역 B) + END ON + + ON 영역 B 행 CLICK DO + // docSeComCd·prgrsSttsComCd·rgtr → view/edit + NAVIGATE 문서 상세 또는 수정 화면 + END ON + +END +``` + +--- + +## 4. 영역 A — 검색 필터 + +구성: **기간 · 구분 · 진행상태** (+ 검색). + +> UI설계 상세설명의 `docSecs` / `docStatusCd`는 레거시·설계 문서 명칭이다. +> **API SoT**는 `ElaprMyDocSearchDto`의 `docSeComCd` / `prgrsSttsComCd`를 사용한다. + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ---------- | ---------------- | ---- | --------------------------- | ----------------- | --------------------------------------------------------------------- | +| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDate` | 신청일(`reqDt`) 기준 검색으로 가정 | +| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDate` | | +| 구분 | Select(공통코드) | N | 전체(미선택) | `docSeComCd` | 상위코드 `ASDOCSE`. UI설계 `docSecs` → API `docSeComCd` | +| 진행상태 | Select(공통코드) | N | 전체(미선택) | `prgrsSttsComCd` | 상위코드 = `{docSeComCd}STTS` (예: `INSTL` → `INSTLSTTS`). UI설계 `docStatusCd` → API 필드명 | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | ----------------------------------------------------------------- | +| 검색 | 영역 A 조건으로 `findMyElaperDocList` 호출 → 영역 B 갱신 (page=0) | + +### 4-3. 콤보 의존 + +| 트리거 | 동작 | +| ----------- | ------------------------------------------------------------------- | +| 구분 변경 | 진행상태 옵션을 `upCd = docSeComCd + "STTS"`로 재조회. 진행상태 선택값 RESET | +| 구분 미선택 | 진행상태 비활성(옵션 비움). `upCd` 조합 불가이므로 조회하지 않음 | + +--- + +## 5. 영역 B — 내 신청 목록 + +### 5-1. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| ------------ | ---------------- | ------------------------------------------------- | +| No | (row index) | 페이지 오프셋 반영 | +| 구분 | `docSeComNm` | 코드 `docSeComCd` (`ASDOCSE`) | +| 신청일 | `reqDt` | 표시 `YYYY-MM-DD` (응답은 `yyyy-MM-dd HH:mm:ss`) | +| 진행상태 | `prgrsSttsComNm` | 텍스트 렌더링 (단계형 프로그레스 미사용) | +| 완료(예정)일 | `cmptnExpctDt` | 표시 `YYYY-MM-DD` | +| 담당자 | `picNm` | | +| 담당자연락처 | `picTelno` | | + +응답에 있으나 그리드에 **표시하지 않음**: + +| Field | 비고 | +| ---------------- | ----------------------------------------------------------------- | +| `docNo` | 문서번호 — 행 클릭 라우팅 키 | +| `docTtl` | 문서명/사업명 — 미표시 | +| `chgCycl` | 변경차수 — 그리드 미표시. SEWER 이동 시 query 파라미터로 사용 | +| `prgrsSttsComCd` | 진행상태 코드 — 컬럼 표시는 `prgrsSttsComNm` | +| `rgtr` | 등록자 UUID — edit 판정용 (`rgtr === userUnqId`) | + +### 5-2. 목록 메타·페이지네이션 + +| 항목 | 매핑 | +| ------------- | ------------------------------------------- | +| 건수 `N개` | `PageResponse.totalElements` | +| page / size | `ElaprMyDocSearchDto`의 `PageableParams` | +| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) | + +### 5-3. 행 상호작용 + +| 동작 | 설명 | +| ------- | -------------------------------------------------------------------------------------------- | +| 행 클릭 | `docSeComCd`·`prgrsSttsComCd`·`rgtr`로 view/edit 분기 후 이동 (`resolveMyPbadmsSprtDetailPath`) | + +**edit 조건** (기존 도메인 목록과 동일): + +| `docSeComCd` | edit 조건 | +| ------------------------ | ---------------------------------------------------------------------- | +| `INSTL` / `FNRS` / `SEWER` | `(prgrsSttsComCd === "SV" \|\| "SL") && rgtr === loggedInUserUnqId` | +| `CNFM` | `prgrsSttsComCd === "CV" && rgtr === loggedInUserUnqId` | + +| `docSeComCd` | edit 경로 | view 경로 | +| ------------ | -------------------------------------------- | --------------------------------------- | +| `FNRS` | `/fnrs-cnsltn/{docNo}/edit` | `/fnrs-cnsltn/{docNo}` | +| `INSTL` | `/instl-sncn/{docNo}/edit` | `/instl-sncn/{docNo}` | +| `CNFM` | `/design-cnstn/{docNo}/edit` | `/design-cnstn/{docNo}` | +| `SEWER` | `/sewer-bsc-plan/{docNo}/edit?chgCycl={n}` | `/sewer-bsc-plan/{docNo}?chgCycl={n}` | +| 그 외 | 이동하지 않음 | | + +- SEWER `chgCycl`: `row.chgCycl ?? 1` +- 조건 불충족 시 항상 view + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 값 | +| --------------- | ------------------------------------------------------------------------------------------ | +| Base prefix | `/as-elapr-service/api/v1/elapr-prgrs-stts` (`ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS`) | +| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) | +| 클라이언트 모듈 | `@/entities/elapr` — `elaprPrgrsSttsService` / `useElaprPrgrsSttsMyDocPage` | + +### 6-2. 내 행정지원 목록 — `findMyElaperDocList` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ----------------- | ------ | --------------- | --------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------- | +| 내 신청 목록 조회 | GET | `{BASE}/my/doc` | Query: `ElaprMyDocSearchDto` + Pageable | `PageResponse` | `elaprPrgrsSttsService.findMyElaperDocList` / `useElaprPrgrsSttsMyDocPage` | + +**Request (`ElaprMyDocSearchDto` + Pageable)** + +| 필드 | 필수 | 설명 | +| ------------------------ | ---- | ------------------------------------------------------------ | +| `startDate` | N | 검색 시작일 `YYYY-MM-DD` | +| `endDate` | N | 검색 종료일 `YYYY-MM-DD` | +| `docSeComCd` | N | 문서구분 (`INSTL` / `FNRS` / `DESIGN` / `SEWER` / `PWRK` 등) | +| `prgrsSttsComCd` | N | 문서진행상태공통코드 | +| `page` / `size` / `sort` | N | Spring Pageable | + +**Response (`MyAdssDocResponseDto`)** + +| 필드 | 타입 | 설명 | +| ---------------- | ------- | ------------------------------------------- | +| `docNo` | string | 문서번호 | +| `docTtl` | string? | 문서명/사업명 | +| `docSeComCd` | string? | 문서구분 코드 | +| `docSeComNm` | string? | 문서구분 코드명 (`ASDOCSE`) | +| `reqDt` | string? | 신청일시 `yyyy-MM-dd HH:mm:ss` | +| `prgrsSttsComCd` | string? | 문서진행상태 코드 | +| `prgrsSttsComNm` | string? | 문서진행상태 코드명 | +| `cmptnExpctDt` | string? | 완료(예정)일 `yyyy-MM-dd` | +| `picNm` | string? | 담당자 성명 | +| `picTelno` | string? | 담당자 연락처 | +| `chgCycl` | number? | 변경차수 | +| `rgtr` | string? | 등록자 UUID | + +### 6-3. 공통코드 (콤보) + +| 용도 | 상위코드 | API | 비고 | +| -------- | ------------ | --------------------------- | ----------------------------- | +| 구분 | `ASDOCSE` | `useCodeList("ASDOCSE")` 등 | 기존 관리 화면과 동일 | +| 진행상태 | `{docSeComCd}STTS` | `useCodeList(docSeComCd + "STTS")` | 예: `INSTLSTTS`, `FNRSSTTS`, `SEWERSTTS`. 구분 미선택 시 미호출 | + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- UI: `@common/components` — DatePicker, Select/`CodeSelect`, Button, DataTable, Pagination +- 목록: `@tanstack/react-table` + `useDataTable` (기존 `instl-list` / `fnrs-list` 패턴) +- 데이터: React Query `useElaprPrgrsSttsMyDocPage(params)` +- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬) +- 진행상태 컬럼: `prgrsSttsComNm` 텍스트 렌더링 (단계형 프로그레스 미사용) +- 행 클릭: `docSeComCd` + `prgrsSttsComCd` + `rgtr`로 view/edit 분기 (`SEWER`는 `chgCycl` query 포함) + +### 7-2. 앱·파일 배치 + +| 구분 | 경로 | +| ------------ | -------------------------------------------------------------- | +| 라우트 | `app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx` | +| 페이지 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list-page.tsx` | +| 목록 그리드 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx` | +| 경로 해석 | `pages/my-pbadms-sprt/lib/resolve-detail-path.ts` | +| 테이블 컬럼 | `pages/my-pbadms-sprt/model/my-pbadms-sprt-table-columns.tsx` | +| Entity API | `entities/elapr/api/elapr-prgrs-stts-service.ts` | +| Entity Hook | `entities/elapr/api/queries.ts` → `useElaprPrgrsSttsMyDocPage` | +| Entity Model | `entities/elapr/model/elapr-prgrs-stts-interface.ts` | + +### 7-3. 영역별 UI 매핑 + +| 영역 | 컴포넌트 | 데이터 | +| ---- | -------------------------- | ------------------------------------------ | +| A | `my-pbadms-sprt-list-page` | 로컬 state → `ElaprMyDocSearchDto` | +| B | DataTable + columns | `useElaprPrgrsSttsMyDocPage` → `content[]` | + +### 7-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ----------- | -------------------------------------------------------------------- | +| 다이얼로그 | 없음 (목록 전용) | +| 목록 라우트 | `/my-pbadms-sprt` | +| 상세 이동 | §5-3 표와 동일 (view/edit + `rgtr` 소유자 검사, SEWER `chgCycl`) | + +### 7-5. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + SET 기간 = 최근 1개월 + CALL findMyElaperDocList + DISPLAY 영역 B + + ON [검색] CLICK DO + CALL findMyElaperDocList // → 영역 B 갱신 + END ON + + ON 페이지 CHANGE DO + CALL findMyElaperDocList + END ON + + ON 영역 B 행 CLICK DO + IF NOT docNo OR unsupported docSeComCd THEN + // no-op + ELSE IF canEdit(prgrsSttsComCd, rgtr === userUnqId) THEN + NAVIGATE edit 경로 // SEWER면 ?chgCycl= + ELSE + NAVIGATE view 경로 + END IF + END ON + +END +``` + diff --git a/apps/as/docs/spec/nti-hstry/SPEC.md b/apps/as/docs/spec/nti-hstry/SPEC.md new file mode 100644 index 000000000..eb6fb5463 --- /dev/null +++ b/apps/as/docs/spec/nti-hstry/SPEC.md @@ -0,0 +1,352 @@ +# 화면설계서: 알림이력 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ----------- | -------------------------------------------- | +| 버전 | 1.4 | +| 프로세스명 | UI설계 | +| 화면명 | 알림이력 | +| 화면 ID | AS_SC0205011 | +| 요구사항 ID | NSUR-001-001 | +| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 | +| 메뉴 경로 | 메인 > 행정지원서비스 > 알림이력 | + +--- + +## 2. 화면 목적 + +로그인한 사용자가 **본인에게 발송된 행정지원(ADSS) 알림**을 기간·구분으로 조회한다. 목록의 처리 버튼으로 해당 알림을 읽음 처리한 뒤, 연계된 업무 화면으로 이동한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ 브레드크럼: 메인 > 행정지원서비스 > 알림이력 │ +│ 페이지 타이틀: 알림이력 │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ 기간 [시작일] ~ [종료일] 구분 ▼ [검색] │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 알림 이력 목록 │ +│ 타이틀: 알림 이력 목록 N개 ▼ │ +│ ┌────┬──────────────┬────────────────┬──────────┬────────┬────────┐ │ +│ │ No │ 알림일시 │ 알림내용 │ 연계업무 │ 상태 │ 처리 │ │ +│ ├────┼──────────────┼────────────────┼──────────┼────────┼────────┤ │ +│ │ … │ regDt │ notiTtl │ docSeComNm│ Badge │ 버튼 │ │ +│ └────┴──────────────┴────────────────┴──────────┴────────┴────────┘ │ +│ [페이지네이션] │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + // 기간 기본값: 오늘 기준 최근 1개월 (시작일 = today-1M, 종료일 = today) + CALL findMyAsNotiList(NotiSearchDto + pageable) + DISPLAY 알림 이력 목록 그리드 (영역 B) + + ON [검색] 버튼 CLICK DO + CALL findMyAsNotiList(영역 A 조건 + page=0) + DISPLAY 알림 이력 목록 그리드 (영역 B) + END ON + + ON 페이지네이션 CHANGE DO + CALL findMyAsNotiList(영역 A 조건 + page/size) + DISPLAY 알림 이력 목록 그리드 (영역 B) + END ON + + ON 영역 B [처리] 버튼 CLICK DO + CALL readNoti(notiId) + // UI설계: 해당 알림에 연계된 URL로 이동 + NAVIGATE notiUrl + END ON + +END +``` + +--- + +## 4. 영역 A — 검색 필터 + +구성: **기간 · 구분** (+ 검색). + +서버 `findMyAsNotiList` 고정 정책 (클라이언트 미전달): + +- `recvUnqId` = 로그인 사용자 UUID +- `delYn` = `N` +- `notiType` = `ADSS` (행정지원 전용 API) + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| ---------- | ---------- | ---- | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ | +| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDt` | `NotiSearchDto.startDt` | +| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDt` | `NotiSearchDto.endDt` | +| 구분 | Select | N | 전체(미선택) | `docSeComCd` | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `PWRK` (또는 단축 `I`/`F`/`S`/`C`/`P`). 공통코드 `ASDOCSE` | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | -------------------------------------------------------------- | +| 검색 | 영역 A 조건으로 `findMyAsNotiList` 호출 → 영역 B 갱신 (page=0) | + +### 4-3. 콤보·기본값 주의 + +| 항목 | 내용 | +| -------------- | ---------------------------------------------------------- | +| 구분 = 전체 | `docSeComCd` 미전달. `notiType`은 서버가 `ADSS`로 고정 | +| 구분 옵션 소스 | 공통코드 `ASDOCSE` (`docSeComCd` ↔ `docSeComNm`) | +| 기간 | UI설계 상세설명: 기본 최근 1개월. `startDt`/`endDt`로 전달 | + +--- + +## 5. 영역 B — 알림 이력 목록 + +### 5-1. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| -------- | ------------ | --------------------------------------------------------------- | +| No | (row index) | 페이지 오프셋 반영 | +| 알림일시 | `regDt` | 표시 `YYYY-MM-DD HH:mm` (응답 `yyyy-MM-dd HH:mm:ss`) | +| 알림내용 | `notiTtl` | 제목 문자열. UI 샘플은 `[YYYY-MM-DD] …` 접두 | +| 연계업무 | `docSeComNm` | 문서구분 공통코드명 (`docSeComCd` + `@CodeName` `ASDOCSE`) | +| 상태 | (derived) | `Badge` 렌더링. 판정 규칙은 §5-1-1 | +| 처리 | (action) | 상태별 버튼 라벨. 클릭 시 `readNoti` 후 `notiUrl`로 이동 (§5-3) | + +#### 5-1-1. 상태 UI 케이스 (`@common/components` `Badge`) + +판정에 사용: `notiPrcsCmptnYn`, `notReadYn`, `regDt` (오늘 기준 경과일). + +| 조건 | 라벨 | Badge | +| ------------------------------------------------------------------------------ | -------- | ----------------------------- | +| `notiPrcsCmptnYn` = `Y` | 처리완료 | `variant="default"` (primary) | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 초과** | 처리지연 | `color="red"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 이내** | 처리중 | `color="green"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 초과** | 지연 | `color="orange"` | +| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 이내** | 미조회 | `color="yellow"` | + +``` +IF notiPrcsCmptnYn = Y THEN + DISPLAY Badge "처리완료" (primary / variant=default) +ELSE IF notReadYn = Y THEN + IF daysSince(regDt) > 7 THEN + DISPLAY Badge "처리지연" (red) + ELSE + DISPLAY Badge "처리중" (green) + END IF +ELSE // notReadYn = N + IF daysSince(regDt) > 3 THEN + DISPLAY Badge "지연" (orange) + ELSE + DISPLAY Badge "미조회" (yellow) + END IF +END IF +``` + +- `daysSince(regDt)`: 오늘 날짜 − `regDt` 일자 (일 단위) +- `regDt`가 없으면 상태 Badge 표시 전략 TBD (미표시 또는 기본값) + +응답에 있으나 그리드에 **표시하지 않음**: + +| Field | 비고 | +| ----------------- | ---------------------------------------------------------------------- | +| `notiId` | 알림 ID — 행 키 | +| `recvUnqId` | 수신 회원 UUID (서버가 로그인 사용자로 고정) | +| `senderUnqId` | 발신 회원 UUID | +| `notiType` | 알림 종류 (`ADSS`) | +| `notiCn` | 알림 내용 (목록에서는 서버가 제외할 수 있음) | +| `notReadYn` | 확인 여부 `Y`/`N`. 상태 Badge 판정에 사용 (§5-1-1) | +| `notiUrl` | 처리 버튼 이동 URL. 컬럼 미표시 | +| `notiLnkgUnqNo` | 알림연결고유번호 | +| `docSeComCd` | 문서구분 공통코드. 연계업무 표시는 `docSeComNm` 사용 | +| `notiIdntyDt` | 알림 확인 일시 | +| `notiIdntyYn` | 알림 확인 여부 (`notReadYn`과 동일 의미). 상태 판정은 `notReadYn` 사용 | +| `notiPrcsCmptnYn` | 알림 처리 완료 여부. 상태 Badge 판정에 사용 (§5-1-1) | +| `notiPrcsCmptnDt` | 알림 처리 완료 일시 | + +### 5-2. 목록 메타·페이지네이션 + +| 항목 | 매핑 | +| ------------- | ------------------------------------------- | +| 건수 / size ▼ | 상단 우측 `N개` 셀렉트 → `Pageable.size` | +| page / size | `NotiSearchDto`의 `PageableParams` | +| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) | + +### 5-3. 행 상호작용 + +| 동작 | 설명 | +| -------------- | --------------------------------------------------------------------------- | +| 처리 버튼 클릭 | `readNoti(notiId)`로 읽음 처리 후, 해당 알림에 연계된 URL(`notiUrl`)로 이동 | +| 행 클릭 | UI설계에 없음. 처리 버튼만 명시 | + +**처리 버튼 라벨** (상태 Badge §5-1-1 기준): + +| 상태 | 버튼명 | +| -------- | -------- | +| 미조회 | 처리하기 | +| 지연 | 처리하기 | +| 처리중 | 처리하기 | +| 처리지연 | 처리하기 | +| 처리완료 | 상세보기 | + +- `notiUrl`이 비어 있으면 이동하지 않음 (no-op). 읽음 처리는 `notiUrl` 유무와 관계없이 호출 +- 읽음 처리 성공 후 목록 쿼리(`notiBaseKey`)를 invalidate 하여 영역 B 상태를 갱신 + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 값 | +| --------------- | ---------------------------------------------------------------------------------------- | +| Base prefix | `/cms-service/api/v1/noti` (`ENDPOINT.CMS_SERVICE.NOTI`) | +| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) | +| 클라이언트 모듈 | `@/entities/noti` — `notiService` / `useFindMyAsNotiList` / `useReadNoti` | + +### 6-2. 로그인한 사용자 행정지원 알림 목록 — `findMyAsNotiList` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------------------------- | ------ | ------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------ | +| 로그인한 사용자 행정지원 알림 목록 | GET | `{BASE}/as/my/list` | Query: `NotiSearchDto` + Pageable | `PageResponse` | `notiService.findMyAsNotiList` / `useFindMyAsNotiList` | + +**Request (`NotiSearchDto` + Pageable)** + +| 필드 | 필수 | 설명 | +| ------------------------ | ---- | --------------------------------------------------------------------------- | +| `recvUnqId` | N | 수신 회원 UUID. 서버가 로그인 사용자로 덮어씀 | +| `senderUnqId` | N | 발신 회원 UUID. 본 화면 필터에 없음 | +| `notiType` | N | 알림 종류. 본 API는 서버가 `ADSS`로 고정 | +| `notiTtlKeyword` | N | 알림 제목 키워드. 본 화면 필터에 없음 | +| `notiCnKeyword` | N | 알림 내용 키워드. 본 화면 필터에 없음 | +| `notReadYn` | N | 알림 확인 여부 (`Y`/`N`). 본 화면 검색 필터에 없음 | +| `delYn` | N | 삭제 여부. 서버가 `N`으로 고정 | +| `startDt` | N | 시작일자 `YYYY-MM-DD` — 영역 A 기간(시작) | +| `endDt` | N | 종료일자 `YYYY-MM-DD` — 영역 A 기간(종료) | +| `docSeComCd` | N | 문서 구분 공통 코드 (`INSTL`/`FNRS`/`SEWER`/`CNFM`/`PWRK` 등) — 영역 A 구분 | +| `page` / `size` / `sort` | N | Spring Pageable | + +**Response (`NotiAsUserResponseDto`)** + +| 필드 | 타입 | 설명 | +| ----------------- | ------- | ------------------------------------------------------- | +| `notiId` | number | 알림 ID | +| `recvUnqId` | string | 수신 회원 UUID | +| `senderUnqId` | string | 발신 회원 UUID | +| `notiType` | string | 알림 종류 (`ADSS`) | +| `notiTtl` | string | 알림 제목 | +| `notiCn` | string? | 알림 내용 (목록에서 제외될 수 있음) | +| `notReadYn` | string? | 알림 확인 여부 (`Y`/`N`). 상태 Badge 판정 | +| `regDt` | string? | 등록 일시 `yyyy-MM-dd HH:mm:ss`. 상태 Badge 경과일 기준 | +| `notiUrl` | string? | 알림 URL 주소 | +| `notiLnkgUnqNo` | string? | 알림연결고유번호 | +| `docSeComCd` | string? | 문서구분 공통코드 | +| `docSeComNm` | string? | 문서구분 공통코드명 (`ASDOCSE`) | +| `notiIdntyDt` | string? | 알림 확인 일시 `yyyy-MM-dd HH:mm:ss` | +| `notiIdntyYn` | string? | 알림 확인 여부 (`Y`/`N`) | +| `notiPrcsCmptnYn` | string? | 알림 처리 완료 여부 (`Y`/`N`). 상태 Badge 판정 | +| `notiPrcsCmptnDt` | string? | 알림 처리 완료 일시 `yyyy-MM-dd HH:mm:ss` | + +### 6-3. 알림 읽음 처리 — `readNoti` + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------------- | ------ | ---------------------- | ----------------------- | ---------------- | -------------------------------------- | +| 알림 읽음 처리 | PUT | `{BASE}/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` | + +**Request** + +| 필드 | 위치 | 필수 | 설명 | +| -------- | ---- | ---- | ------- | +| `notiId` | Path | Y | 알림 ID | + +Body 없음. + +**Response** + +| 타입 | 설명 | +| ------ | ------------------------------ | +| number | 읽음 처리된 알림 ID (`notiId`) | + +화면 매핑: 영역 B [처리] 버튼 클릭 시 호출. 성공 시 `notiBaseKey` invalidate → `findMyAsNotiList` 재조회. + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- UI: `@common/components` — DatePicker, Select, Button, DataTable, Pagination, **Badge** +- 목록: `@tanstack/react-table` + `useDataTable` (기존 `my-pbadms-sprt-list` 패턴) +- 데이터: React Query `useFindMyAsNotiList(params)`, `useReadNoti()` +- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬) +- 처리 컬럼: 행 클릭이 아니라 **버튼 클릭**으로 `readNoti` 후 `notiUrl` 이동 +- 상태 컬럼: §5-1-1 규칙으로 `Badge` 렌더링 (`notiPrcsCmptnYn` / `notReadYn` / `regDt`) +- 연계업무: `docSeComNm` 표시 + +### 7-2. 앱·파일 배치 + +| 구분 | 경로 | +| ------------ | --------------------------------------------------------------------- | +| 라우트 | `app/(main)/(with-base-layout)/noti/page.tsx` | +| 페이지 | `pages/noti/ui/noti-list-page.tsx` | +| 목록 그리드 | `pages/noti/ui/noti-list.tsx` | +| 테이블 컬럼 | `pages/noti/model/noti-table-columns.tsx` | +| 상태 판정 | `pages/noti/lib/resolve-noti-status.ts` | +| Entity API | `entities/noti/api/noti-service.ts` | +| Entity Hook | `entities/noti/api/queries.ts` → `useFindMyAsNotiList`, `useReadNoti` | +| Entity Model | `entities/noti/model/noti-interface.ts` | + +화면 레이어(`pages/noti`)·Entity(`findMyAsNotiList`, `readNoti`) 구현 완료. + +### 7-3. 영역별 UI 매핑 + +| 영역 | 컴포넌트 | 데이터 | +| ---- | ------------------- | -------------------------------------------------------------- | +| A | `noti-list-page` | 로컬 state → `NotiSearchDto` | +| B | DataTable + columns | `useFindMyAsNotiList` → `content[]`. 처리 버튼 → `useReadNoti` | + +### 7-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ----------- | --------------------------------------------------------------------- | +| 다이얼로그 | 없음 (목록 전용) | +| 목록 라우트 | `/noti` | +| 상세 이동 | 처리 버튼 → `readNoti(notiId)` 후 `notiUrl` (절대/상대 경로 형식 TBD) | + +### 7-5. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + SET 기간 = 최근 1개월 // startDt / endDt 전달 + CALL findMyAsNotiList + DISPLAY 영역 B + + ON [검색] CLICK DO + CALL findMyAsNotiList // → 영역 B 갱신 + END ON + + ON 페이지 CHANGE DO + CALL findMyAsNotiList + END ON + + ON 영역 B [처리] 버튼 CLICK DO + CALL readNoti(notiId) // → 영역 B 상태 갱신 (invalidate) + IF NOT notiUrl THEN + // no-op (읽음 처리는 수행됨) + ELSE + NAVIGATE notiUrl + END IF + END ON + +END +``` diff --git a/apps/as/docs/spec/previous-plan-lookup/SPEC.md b/apps/as/docs/spec/previous-plan-lookup/SPEC.md new file mode 100644 index 000000000..927121116 --- /dev/null +++ b/apps/as/docs/spec/previous-plan-lookup/SPEC.md @@ -0,0 +1,441 @@ +# 화면설계서: 이전기본계획조회 (기본 계획 찾기) + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | --------------------------------------------------------------- | +| 버전 | 0.2 | +| 프로세스명 | UI설계 | +| 화면명 | 이전기본계획조회 (기본 계획 찾기) | +| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 | +| 메뉴 경로 | 하수도기본계획 등록·수정 > 사업개요 > [이전기본계획조회] Dialog | + +--- + +## 2. 화면 목적 + +하수도기본계획 등록·수정 화면에서 **이전(기수립) 기본계획** 수치와 관련 보고서를 **조회·다운로드**한다. 시도·구군·연도·처리시설을 선택한 뒤 총설·처리구역별 현황을 표시하고, 분류 트리에서 기본계획 파일을 내려받는다. **부모 사업개요 폼에는 수치를 반영하지 않는다**(레거시 `bscPlnPopup.jsp`와 동일). + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +부모: 하수도기본계획 등록/수정 폼 → 사업개요 섹션의 `[이전기본계획조회]` 버튼으로 Dialog 오픈. + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ Dialog 타이틀: 기본 계획 찾기 [닫기] │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ 시도 ▼ 구군 ▼ 연도 ▼ 처리시설 ▼ [조회] │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 기본계획파일 │ +│ ┌──────────────────────┬────────────────────────────────────────────────────┐ │ +│ │ [영역 B-1] 분류 트리 │ [영역 B-2] 파일 목록 │ │ +│ │ (약 40%) │ (약 60%) │ │ +│ │ └ 노드… │ 파일명 / 다운로드 │ │ +│ └──────────────────────┴────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 C] 총설 │ +│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │ +│ │ 구분 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ 기준 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ │ +│ ├────────┼───────┼───────┼───────┼───────┼────────┼───────┼───────┼───────┼───────┤ │ +│ │계획목표│ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ │ +│ │년도 │ │ │ │ │ │ │ │ │ │ │ +│ └────────┴───────┴───────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┘ │ +├─────────────────────────────────────────────────────────────────────────────────┤ +│ [영역 D] 처리구역별 현황 │ +│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │ +│ │ 구분 │ 1~4단계(+연도헤더) │ 기준 │ 1~4단계(+연도헤더) │ │ +│ ├────────┼────────────────────────────────┼────────┼────────────────────────────────┤ │ +│ │ 하수처리인구 … 배제방식 (읽기 전용 행) │ │ +│ └────────┴────────────────────────────────┴────────┴────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 부모 폼 — 사업개요 + ON [이전기본계획조회] 버튼 CLICK DO + OPEN PreviousPlanLookupDialog + // 프리필: 부모 폼 sewerBscPlan.sido / gugun / admdstCd + CALL findSidoCombo() + DISPLAY 시도 옵션 (영역 A) + IF 시도 프리필 THEN + CALL findGugunCombo(sidoCd) + DISPLAY 구군 옵션 (영역 A) + CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm }) + // TBD: useLvNowDmgMtncGnsvy enabled — level별 필수 파라미터만 검사하도록 조정 + IF 구군 프리필 THEN + CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm }) + DISPLAY 연도 옵션 (영역 A) // children 중 level=3 노드의 gnsvyYr + CALL getClsfSystm({ clsfSystmNm: gugunNm }) + DISPLAY 분류 트리 (영역 B-1) + END IF + END IF + END ON + + // 영역 A: 시도 변경 + ON 영역 A 시도 CHANGE DO + RESET 구군 / 연도 / 처리시설 + CALL findGugunCombo(sidoCd) + CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm }) + // 구군 미선택 → 연도·처리시설 옵션 비움, 트리 빈 안내 + END ON + + // 영역 A: 구군 변경 + ON 영역 A 구군 CHANGE DO + RESET 연도 / 처리시설 + CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm }) + DISPLAY 연도 옵션 (영역 A) // level=3 노드 → gnsvyYr + CALL getClsfSystm({ clsfSystmNm: sggNm }) + DISPLAY 분류 트리 (영역 B-1) + END ON + + // 영역 A: 연도 변경 + ON 영역 A 연도 CHANGE DO + RESET 처리시설 + CALL getLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr }) + DISPLAY 처리시설 옵션 (영역 A) // level=3 노드 children → swtrmtFcltCd / swtrmtFcltNm + END ON + + // 영역 A: 조회 + ON [조회] 버튼 CLICK DO + VALIDATE 시도, 구군, 연도(targetYear), 처리시설(swtrmtFcltCd) + CALL getNowDmgMtncGnsvy({ swtrmtFcltCd, ctpvNm, sggNm, targetYear }) + DISPLAY 총설 (영역 C) // 응답[0]의 planYr / planYrDt* / planYrDc* + DISPLAY 처리구역별 현황 (영역 D) // 응답[0]의 지표 Dt1~4 / base / Dc1~4 + END ON + + // 영역 B: 트리 노드 선택 + ON 영역 B-1 노드 CLICK DO + CALL getFile({ clsfSystmSn, sort, vetPrcsYn }) // Response DTO TBD + DISPLAY 파일 목록 (영역 B-2) + END ON + + // 영역 B: 파일 다운로드 + ON 영역 B-2 파일 CLICK DO + CALL getFilePrcs(fileSn) // 파일 메타(경로·제목) — Response TBD + CALL fileDownload({ filePath, fileName }) + // 브라우저 다운로드 트리거 + END ON + + ON [닫기] DO + CLOSE PreviousPlanLookupDialog + // 부모 폼 값 변경 없음 + END ON + +END +``` + +--- + +## 4. 영역 A — 검색 필터 + +구성: **시도 · 구군 · 연도 · 처리시설** (+ 조회). + +- 시도·구군: `adstdstr` 콤보 (기존 스펙 유지) +- 연도·처리시설: 각각 `useLvNowDmgMtncGnsvy`를 **별도 호출**. `level`에 따라 반환 계층이 달라진다. + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------- | --------- | ---- | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------- | +| 시도 | Select | Y | 부모 `sewerBscPlan.sido` | `ctpvNm`(표시명), 코드는 `adstDstrCd` | `useFindSidoCombo` | +| 구군 | Select | Y | 부모 `sewerBscPlan.gugun` | `sggNm`(표시명), 코드는 `adstDstrCd` | `useFindGugunCombo(sidoCd)` | +| 연도 | Select | Y | 없음 | `gnsvyYr` / 조회 시 `targetYear` | `useLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })` | +| 처리시설 | Select | Y | 없음 | `swtrmtFcltCd` ← 옵션 value, 라벨 `swtrmtFcltNm` | `useLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })` | + +> 처리시설은 `bns-if` `useFacilityList`를 **사용하지 않는다**. + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | -------------------------------------------------------- | +| 조회 | 필수값 검증 후 `getNowDmgMtncGnsvy` 호출 → 영역 C·D 갱신 | +| 닫기 | Dialog 종료 (부모 미반영) | + +### 4-3. `lvNowDmgMtncGnsvy` level 캐스케이드 + +| 트리거 | Request `level` | Request 파라미터 | UI에 쓰는 응답 | 옵션 추출 | +| --------- | --------------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------ | +| 시도 선택 | `"1"` | `ctpvNm` | (하위 초기화용) | 연도·처리시설 Select 리셋. 구군은 `findGugunCombo` | +| 구군 선택 | `"2"` | `ctpvNm`, `sggNm` | 연도 Select | 트리에서 `level === 3` 노드의 `gnsvyYr` | +| 연도 선택 | `"3"` | `ctpvNm`, `sggNm`, `gnsvyYr` | 처리시설 Select | 선택 연도(`level === 3`) 노드의 `children[]` → `swtrmtFcltCd` / `swtrmtFcltNm` | + +상위 변경 시 하위 선택값·옵션을 RESET한다 (시도→구군·연도·시설 / 구군→연도·시설 / 연도→시설). + +--- + +## 5. 영역 B — 기본계획파일 + +### 5-1. 트리 (B-1) + +| 표시명 | Field | 비고 | +| ------ | --------------- | ------------------------ | +| 분류명 | `clsfSystmNm` | `ClsfSystmResponseDto` | +| 노드키 | `clsfSystmSn` | 선택 시 파일 목록 조회 | +| 상위키 | `upClsfSystmSn` | 클라이언트에서 트리 조립 | +| 정렬 | `sortSeq` | | +| 경로 | `path` | | + +- 조회 파라미터: `clsfSystmNm` = 선택 구군명(레거시와 동일). `upClsfSystmSn`은 하위 전개 시 TBD. +- 시도·구군 미선택 시: 「시도와 구군을 선택하세요.」 안내. + +### 5-2. 파일 목록 (B-2) + +| 표시명 | Field | 비고 | +| ------ | ----- | ------------------------------------------------------------------- | +| 파일명 | TBD | `getFile` Response DTO가 `unknown` — 백엔드 스키마 확정 후 매핑 | +| (기타) | TBD | 레거시: 트리 선택 후 목록 표시, 클릭 시 `fileprcs` → `fileDownload` | + +### 5-3. 버튼·상호작용 + +| 동작 | 설명 | +| -------------- | -------------------------------------------------------------- | +| 트리 노드 클릭 | `getFile({ clsfSystmSn, … })` → B-2 목록 | +| 파일 클릭 | `getFilePrcs(fileSn)` → `fileDownload({ filePath, fileName })` | +| 빈 목록 | 「트리에서 항목을 선택하세요.」 / 「파일이 없습니다.」 | + +--- + +## 6. 영역 C — 총설 + +### 6-1. 테이블 구조 + +| 행(구분) | 열 매핑 (당초1~~4 / 기준 / 변경1~~4) | +| ------------ | ------------------------------------------------------------------------------ | +| 계획목표년도 | `planYrDt1planYrDt4` ~~/~~ `planYr` ~~/~~ `planYrDc1planYrDc4` (+ `년` 접미사) | + +- 헤더: 당초(1~~4단계) · 기준 · 변경(1~~4단계) +- 데이터 소스: `getNowDmgMtncGnsvy` 응답 배열의 **첫 번째** 요소 +- 읽기 전용 + +--- + +## 7. 영역 D — 처리구역별 현황 + +### 7-1. 행 정의 (표시명 → Field 접두사) + +각 행은 `Dt1Dt4` ~~/ (접미사 없음=기준) /~~ `Dc1Dc4` 셀로 표시한다. 숫자는 천단위 콤마, 없으면 `-`. + +| 표시명 | Field 접두사 | 비고 | +| ------------------------ | -------------------- | ---------------------------------------------- | +| 하수처리인구(인) | `swtrmtPpltnCnt` | | +| 자연적하수처리인구수(인) | `natrSwtrmtPpltnCnt` | | +| 사회적하수처리인구수(인) | `scilSwtrmtPpltnCnt` | | +| 하수처리구역면적 | `swtrmtZoneArea` | | +| 물사용량원단위 | `wtrUsqtyOrgnUnit` | | +| 오수전환율(%) | `drtwSwtcRt` | | +| 변동부하율(%) | `flctnLodRt` | | +| 오수원단위 | `drtwOrgnUnit` | | +| 계획하수량합계(㎥/일) | `planSwageQntSum` | | +| 생활오수(㎥/일) | `lvngDrtwSum` | | +| 지하수(㎥/일) | `ugwtSum` | | +| 공장폐수(㎥/일) | `fctryWwtSum` | | +| 관광오수(㎥/일) | `toursmDrtwSum` | | +| 군부대(㎥/일) | `mtbsSum` | | +| 연계처리수(㎥/일) | `linkPrcsCntSum` | | +| 기타오수(㎥/일) | `etcDrtwSum` | | +| 지하수사용량(㎥/일) | `ugwtUseSum` | | +| 시설용량(㎥/일) | `fcltCpctSum` | | +| 배제방식 | `exclsMethExpln` | 문자열; 단계별 `*Dt*`/`*Dc*` 존재 시 동일 규칙 | + +헤더 3행째 연도: 영역 C와 동일하게 `planYrDt*` / `planYr` / `planYrDc*` 표시. + +--- + +## 8. API/DTO 명세 + +### 8-1. 공통 + +| 항목 | 값 | +| --------------- | ---------------------------------------------------- | +| DC Base | `/dc-service/api/v1` (`ENDPOINT.DC_SERVICE`) | +| AS adstdstr | `/as-service/api/v1/adstdstr` | +| 클라이언트 모듈 | `@/entities/dc` (`dcService`), `@/entities/adstdstr` | + +> 영역 A 처리시설은 `bns-if`를 사용하지 않음. + +### 8-2. 시도·구군 콤보 (non-DC) + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| -------- | ------ | ----------------------------------- | ------------------------ | -------------------------------- | ---------------------------------------------------------- | +| 시도목록 | GET | `/as-service/api/v1/adstdstr/sido` | — | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findSidoCombo` / `useFindSidoCombo` | +| 구군목록 | GET | `/as-service/api/v1/adstdstr/gugun` | Query: `sido` (시도코드) | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findGugunCombo` / `useFindGugunCombo` | + +### 8-3. 연도·처리시설 계층 조회 — DC + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| --------------- | ------ | -------------------------------------- | ---------------------------- | -------------------------------- | --------------------------------------------------------- | +| 계층형 계획목록 | GET | `/dc-service/api/v1/lvNowDmgMtncGnsvy` | `LvNowDmgMtncGnsvySearchDto` | `LvNowDmgMtncGnsvyResponseDto[]` | `dcService.getLvNowDmgMtncGnsvy` / `useLvNowDmgMtncGnsvy` | + +연도 Select·처리시설 Select는 **각각** `useLvNowDmgMtncGnsvy`를 호출한다 (`level`만 다르게). + +**Request (**`LvNowDmgMtncGnsvySearchDto`**)** + +| 필드 | 시도(`level=1`) | 구군→연도(`level=2`) | 연도→시설(`level=3`) | +| --------- | --------------- | -------------------- | -------------------- | +| `level` | `"1"` | `"2"` | `"3"` | +| `ctpvNm` | 필수 | 필수 | 필수 | +| `sggNm` | — | 필수 | 필수 | +| `gnsvyYr` | — | — | 필수 | + +**Response 노드 필드 (**`LvNowDmgMtncGnsvyResponseDto`**, 부분 optional)** + +| 필드 | 등장 level | 설명 | +| -------------- | ---------- | ------------------------ | +| `level` | 1~3 | 계층 깊이 | +| `ctpvNm` | 1 | 시도명 | +| `sggNm` | 2 | 구군명 | +| `gnsvyYr` | 3 | 연도 → 연도 Select value | +| `swtrmtFcltCd` | 시설(리프) | 처리시설 코드 | +| `swtrmtFcltNm` | 시설(리프) | 처리시설명 | +| `children` | 1~3 | 하위 노드 배열 | + +> **TBD**: 현재 `useLvNowDmgMtncGnsvy` `enabled`가 `level && ctpvNm && sggNm && gnsvyYr`로 고정되어 `level=1`/`2` 호출이 막힘. level별 필수 파라미터만 검사하도록 수정 필요. +> +> 예: `level=1` → `ctpvNm` / `level=2` → `ctpvNm && sggNm` / `level=3` → `ctpvNm && sggNm && gnsvyYr` + +### 8-4. 기본계획 수치 조회 — DC + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------- | ------ | ------------------------------------ | -------------------------- | ------------------------------ | ----------------------------------------------------- | +| 현재·단계별 수치 | GET | `/dc-service/api/v1/nowDmgMtncGnsvy` | `NowDmgMtncGnsvySearchDto` | `NowDmgMtncGnsvyResponseDto[]` | `dcService.getNowDmgMtncGnsvy` / `useNowDmgMtncGnsvy` | + +**Request (**`NowDmgMtncGnsvySearchDto`**)** + +| 필드 | 매핑 | +| -------------- | ----------------------- | +| `swtrmtFcltCd` | 영역 A 처리시설 | +| `ctpvNm` | 영역 A 시도 표시명 | +| `sggNm` | 영역 A 구군 표시명 | +| `targetYear` | 영역 A 연도 (`gnsvyYr`) | + +**Response**: 지표 필드 + `Dt1Dt4` ~~/~~ `Dc1Dc4` 접미사 스테이지 필드 (`NowDmgMtncGnsvyResponseDto`). UI는 `data[0]` 사용. + +### 8-5. 분류체계 — DC + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ------------ | ------ | ------------------------------ | -------------------- | ------------------------ | ----------------------------------------- | +| 분류트리조회 | GET | `/dc-service/api/v1/clsfSystm` | `ClsfSystmParamsDto` | `ClsfSystmResponseDto[]` | `dcService.getClsfSystm` / `useClsfSystm` | + +**Request**: `clsfSystmNm?`, `upClsfSystmSn?` +**Response**: `clsfSystmSn`, `upClsfSystmSn`, `clsfSystmNm`, `sortSeq`, `droppAble`, `path` + +> 레거시 경로 `/clsf` → 현재 클라이언트는 `/clsfSystm`. + +### 8-6. 파일 목록·처리·다운로드 — DC + +| 기능 | Method | URL | Request DTO / Params | Response DTO | API | +| ------------ | ------ | --------------------------------- | ------------------------------------------------------- | ----------------- | -------------------------------------------- | +| 파일목록 | GET | `/dc-service/api/v1/file` | `FileParamsDto` (`sort?`, `clsfSystmSn?`, `vetPrcsYn?`) | `unknown` **TBD** | `dcService.getFile` / `useFile` | +| 파일처리정보 | GET | `/dc-service/api/v1/fileprcs` | Query: `fileSn` | `unknown` **TBD** | `dcService.getFilePrcs` / `useFilePrcs` | +| 파일다운로드 | POST | `/dc-service/api/v1/fileDownload` | `FileDownloadParamsDto` (`filePath?`, `fileName?`) | `unknown` **TBD** | `dcService.fileDownload` / `useFileDownload` | + +> **TBD**: 파일 관련 Response 스키마·다운로드 blob/헤더 처리·`fileDownload` POST body vs query 전달 방식은 백엔드/게이트웨이 확인 후 확정. + +--- + +## 9. UI 구현 가이드 + +### 9-1. 기술 스택·원칙 + +- UI: `@common/components` — `Dialog`, `Select`, `Button`, `Table`, `Skeleton` +- 상태: Dialog 로컬 state + React Query (`entities/dc`, `adstdstr`) +- 영역 A 연도·처리시설: `useLvNowDmgMtncGnsvy`를 level별 **두 번**(또는 시도 포함 시 세 번) 호출 +- 테이블: 읽기 전용 고정 행 → `@tanstack/react-table` / `DataTable` **불필요** +- 트리: jsTree 미사용 — 재귀 들여쓰기 트리 +- 번들: `PreviousPlanLookupDialog`는 outline에서 `next/dynamic` 로드 권장 +- 부모 폼: **수치 미반영** (조회·다운로드 only) +- 1차 구현 시 DC 미연동이면 `@faker-js/faker` mock + TODO → 이후 `dcService` 훅으로 교체 + +### 9-2. 앱·파일 배치 + +| 구분 | 경로 | +| ------------ | ------------------------------------------------------------------------ | +| 진입 버튼 | `pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx` | +| Dialog | `pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx` | +| 필터 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx` | +| 파일 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx` | +| 총설 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-summary-table.tsx` | +| 처리구역 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-area-table.tsx` | +| 셀 매핑 유틸 | `pages/sewer-bsc-plan/lib/previous-plan-table-utils.ts` | +| mock(선택) | `pages/sewer-bsc-plan/lib/previous-plan-mock.ts` | +| DC entity | `entities/dc/api/dc-service.ts`, `queries.ts`, `model/dc-interface.ts` | +| 레거시 참조 | `adssNewOld/.../cmmn/bscPlnPopup.jsp` | + +### 9-3. 영역별 UI 매핑 + +| 영역 | 컴포넌트 | 데이터 | +| ---- | ------------------------------ | -------------------------------------------------------------- | +| A | `previous-plan-search-filters` | adstdstr + `useLvNowDmgMtncGnsvy`(level 1·2·3) | +| B | `previous-plan-file-section` | `useClsfSystm` + `useFile` + `useFilePrcs` + `useFileDownload` | +| C | `previous-plan-summary-table` | `useNowDmgMtncGnsvy` / 조회 결과 state | +| D | `previous-plan-area-table` | 동일 | + +### 9-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ------------ | ------------------------------------------------------------------------ | +| 다이얼로그명 | `PreviousPlanLookupDialog` / 타이틀 `기본 계획 찾기` | +| 오픈 | 사업개요 `[이전기본계획조회]` | +| 라우트 | 없음 (모달). 부모: `/sewer-bsc-plan/regist`, `/sewer-bsc-plan/[id]/edit` | +| 선택 반영 | 없음 | + +### 9-5. 데이터 흐름 (구현 요약) + +``` +BEGIN + + ON [이전기본계획조회] CLICK DO + OPEN PreviousPlanLookupDialog (sido/gugun 프리필) + END ON + + ON 시도 CHANGE DO + CALL findGugunCombo + CALL getLvNowDmgMtncGnsvy(level=1) + RESET 연도·처리시설 + END ON + + ON 구군 CHANGE DO + CALL getLvNowDmgMtncGnsvy(level=2) // → 연도 옵션 + CALL getClsfSystm + RESET 처리시설 + END ON + + ON 연도 CHANGE DO + CALL getLvNowDmgMtncGnsvy(level=3) // → 처리시설 옵션 + END ON + + ON [조회] CLICK DO + VALIDATE + CALL getNowDmgMtncGnsvy + DISPLAY 영역 C · 영역 D + END ON + + ON 트리 노드 / 파일 CLICK DO + CALL getFile → getFilePrcs → fileDownload // Response TBD + END ON + +END +``` + +--- + +## 10. 미확정 (TBD) 추적 + +| ID | 항목 | 비고 | +| --- | -------------------------------------------------------- | -------------------------------------------------------------------- | +| T1 | `useLvNowDmgMtncGnsvy` enabled을 level별 필수값으로 분기 | `1`: ctpvNm / `2`: +sggNm / `3`: +gnsvyYr | +| T2 | `getFile` / `getFilePrcs` / `fileDownload` Response DTO | 현재 `unknown` | +| T3 | 파일 다운로드 UX (blob, Content-Disposition, 게이트웨이) | 레거시 apigateway 직접 호출과 차이 | +| T4 | `clsfSystm`의 `upClsfSystmSn` 점진 로드 여부 | flat 목록 vs 계층 API | +| T5 | 1차 mock vs 즉시 `dcService` 실연동 | 마이그레이션 플랜은 mock 우선, entity는 준비됨 | +| T6 | 부모 폼 수치 반영 | 요구 없음(레거시 동일). 요청 시 별도 스펙 | +| T7 | `level=1` 응답 UI 활용 여부 | 시도 선택 시 호출하나 구군은 adstdstr — 응답은 캐시·검증용일 수 있음 | diff --git a/apps/as/docs/review-manpower-management/SPEC.md b/apps/as/docs/spec/review-manpower-management/SPEC.md similarity index 100% rename from apps/as/docs/review-manpower-management/SPEC.md rename to apps/as/docs/spec/review-manpower-management/SPEC.md diff --git a/apps/as/middleware.ts b/apps/as/middleware.ts index 94a1d0381..dbd2a53f7 100644 --- a/apps/as/middleware.ts +++ b/apps/as/middleware.ts @@ -1,8 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; -import { AUTH_USER_ID, CLAIM_NAME, PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config"; +import { + AUTH_USER_ID, + CLAIM_NAME, + PORT, + PROXY_HOST, + SERVER_API_URL, + SITE_ID, +} from "@/shared/config"; const serverApiUrl = process.env.SERVER_API_URL; +const dcServiceApiUrl = process.env.DC_SERVICE_API_URL; // CSP 헤더 설정 함수 (img-src만) function addSecurityHeaders(response: NextResponse): NextResponse { @@ -22,7 +30,15 @@ export function middleware(req: NextRequest) { if (pathname.startsWith("/server")) { const queryParams = searchParams.toString(); - const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + + let serverUrl: string | undefined; + if (pathname.includes("/dc-service")) { + serverUrl = dcServiceApiUrl; + } else { + serverUrl = serverApiUrl; + } + + const newUrl = `${serverUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; @@ -34,7 +50,6 @@ export function middleware(req: NextRequest) { return addSecurityHeaders(response); } - // tus-js-client PATCH 요청: Location 헤더로 /server 없이 반환된 경로 직접 처리 // (예: /attach-service/api/v1/tus/upload/{id}) if (pathname.startsWith("/attach-service")) { @@ -56,10 +71,10 @@ export function middleware(req: NextRequest) { return addSecurityHeaders(response); } - // if (pathname === "/") { - // const response = NextResponse.redirect(new URL("/dashboard", req.url)); - // return addSecurityHeaders(response); - // } + if (pathname === "/") { + const response = NextResponse.redirect(new URL("/dashboard", req.url)); + return response; + } logMessages.push("\n--------------------------------\n"); console.log(logMessages.join("\n")); diff --git a/apps/as/next.config.ts b/apps/as/next.config.ts index 8864de0e7..606194380 100644 --- a/apps/as/next.config.ts +++ b/apps/as/next.config.ts @@ -1,8 +1,39 @@ import type { NextConfig } from "next"; +const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000"; + const nextConfig: NextConfig = { + output: "standalone", reactStrictMode: true, transpilePackages: ["@common/assets"], + 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*`, + }, + ], + fallback: [], + }; + }, }; export default nextConfig; diff --git a/apps/as/package.json b/apps/as/package.json index ea9e0545a..ed4016352 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -20,6 +20,7 @@ "@faker-js/faker": "^9.9.0", "@frontend-opensource/use-react-hooks": "^1.1.4", "@hookform/resolvers": "^4.1.3", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.75.5", "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", diff --git a/apps/as/public/favicon.ico b/apps/as/public/favicon.ico new file mode 100644 index 000000000..d304b6981 Binary files /dev/null and b/apps/as/public/favicon.ico differ diff --git a/apps/as/public/images/help.png b/apps/as/public/images/help.png new file mode 100644 index 000000000..e872802e4 Binary files /dev/null and b/apps/as/public/images/help.png differ diff --git a/apps/as/public/images/magnifying-glass-with-warning.png b/apps/as/public/images/magnifying-glass-with-warning.png new file mode 100644 index 000000000..b184da207 Binary files /dev/null and b/apps/as/public/images/magnifying-glass-with-warning.png differ diff --git a/apps/as/public/images/region_map.svg b/apps/as/public/images/region_map.svg new file mode 100644 index 000000000..f327bb9fc --- /dev/null +++ b/apps/as/public/images/region_map.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/as/public/images/region_map_bg.png b/apps/as/public/images/region_map_bg.png new file mode 100644 index 000000000..6f59f58cc Binary files /dev/null and b/apps/as/public/images/region_map_bg.png differ diff --git a/apps/as/src/app/layouts/base-layout.tsx b/apps/as/src/app/layouts/base-layout.tsx index fc84a5e3e..ebe24b41c 100644 --- a/apps/as/src/app/layouts/base-layout.tsx +++ b/apps/as/src/app/layouts/base-layout.tsx @@ -7,6 +7,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; +import { authService } from "@/entities/auth"; import type { MenuItem } from "@/entities/menu"; import { BaseLayout } from "@common/components"; @@ -32,6 +33,7 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) { return ( diff --git a/apps/as/src/app/providers/index.tsx b/apps/as/src/app/providers/index.tsx index 20ec0b72e..2a0ac6e69 100644 --- a/apps/as/src/app/providers/index.tsx +++ b/apps/as/src/app/providers/index.tsx @@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) { - +
+ +
); } diff --git a/apps/as/src/app/styles/globals.css b/apps/as/src/app/styles/globals.css index e557f947b..bf1ecad16 100644 --- a/apps/as/src/app/styles/globals.css +++ b/apps/as/src/app/styles/globals.css @@ -6,10 +6,19 @@ @tailwind components; @tailwind utilities; -html, -body { - color: var(--foreground); - background: var(--background); - font-family: "Noto Sans KR", sans-serif; - font-size: 10px; +@layer base { + :root { + --secondary-background: rgba(0, 0, 0, 0.04); + } + [data-theme="black"] { + --secondary-background: rgba(255, 255, 255, 0.09); + } + + html, + body { + color: var(--foreground); + background: var(--background); + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; + } } diff --git a/apps/as/src/entities/acntg/model/acntg-interface.ts b/apps/as/src/entities/acntg/model/acntg-interface.ts index c1457208d..ea64fc218 100644 --- a/apps/as/src/entities/acntg/model/acntg-interface.ts +++ b/apps/as/src/entities/acntg/model/acntg-interface.ts @@ -16,6 +16,7 @@ export interface AcntgSeMngResponseDto { cdLvNo: number; useYn: string; expln?: string; + sortSeq?: number; rgtr?: string; rgtrNm?: string; mdfr?: string; @@ -39,6 +40,7 @@ export interface AcntgSeMngSaveRequestDto { cdLvNo: number; useYn?: string; expln?: string; + sortSeq?: number; } /** PUT /{acntgSeCd} 요청 (mode는 서비스에서 U로 설정) */ diff --git a/apps/as/src/entities/adstdstr/api/adstdstr-service.ts b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts index b4bb11fae..f282ea67b 100644 --- a/apps/as/src/entities/adstdstr/api/adstdstr-service.ts +++ b/apps/as/src/entities/adstdstr/api/adstdstr-service.ts @@ -13,6 +13,11 @@ async function findSidoCombo(): Promise { return response.data; } +async function findUserSidoCombo(): Promise { + const response = await client.get(`${BASE}/user/sido`); + return response.data; +} + async function findGugunCombo(sido: string): Promise { const response = await client.get(`${BASE}/gugun`, { params: { sido }, @@ -20,7 +25,16 @@ async function findGugunCombo(sido: string): Promise { + const response = await client.get(`${BASE}/user/gugun`, { + params: { sido }, + }); + return response.data; +} + export const cmCdAdstDstrService = { findSidoCombo, + findUserSidoCombo, findGugunCombo, + findUserGugunCombo, } satisfies CmCdAdstDstrService; diff --git a/apps/as/src/entities/adstdstr/api/queries.ts b/apps/as/src/entities/adstdstr/api/queries.ts index 353e25cfe..2556ec6db 100644 --- a/apps/as/src/entities/adstdstr/api/queries.ts +++ b/apps/as/src/entities/adstdstr/api/queries.ts @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; +import { IUserInfo } from "@/shared/store"; import { cmCdAdstDstrService } from "./adstdstr-service"; @@ -13,6 +14,16 @@ export function useFindSidoCombo() { }); } +export function useFindUserSidoCombo(userInfo?: IUserInfo | null) { + const adstDstrCdByUser = userInfo?.laeSeCd?.substring(2, 4); + return useQuery({ + queryKey: [...cmCdAdstDstrBaseKey, "findUserSidoCombo", adstDstrCdByUser ?? "all"], + queryFn: () => cmCdAdstDstrService.findUserSidoCombo(), + select: (data) => + adstDstrCdByUser ? data.filter((item) => item.adstDstrCd === adstDstrCdByUser) : data, + }); +} + export function useFindGugunCombo(sido: string) { return useQuery({ queryKey: [...cmCdAdstDstrBaseKey, "findGugunCombo", sido], @@ -20,3 +31,11 @@ export function useFindGugunCombo(sido: string) { enabled: Boolean(sido), }); } + +export function useFindUserGugunCombo(sido: string) { + return useQuery({ + queryKey: [...cmCdAdstDstrBaseKey, "findUserGugunCombo", sido], + queryFn: () => cmCdAdstDstrService.findUserGugunCombo(sido), + enabled: Boolean(sido), + }); +} diff --git a/apps/as/src/entities/adstdstr/index.ts b/apps/as/src/entities/adstdstr/index.ts index da1dbd132..2fffe5a79 100644 --- a/apps/as/src/entities/adstdstr/index.ts +++ b/apps/as/src/entities/adstdstr/index.ts @@ -1,5 +1,10 @@ export { cmCdAdstDstrService } from "./api/adstdstr-service"; -export { useFindGugunCombo, useFindSidoCombo } from "./api/queries"; +export { + useFindGugunCombo, + useFindSidoCombo, + useFindUserGugunCombo, + useFindUserSidoCombo, +} from "./api/queries"; export type { CmCdAdstDstrComboResponseDto, CmCdAdstDstrService, diff --git a/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts index 72cb673dc..1eaa14bff 100644 --- a/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts +++ b/apps/as/src/entities/adstdstr/model/adstdstr-interface.ts @@ -7,5 +7,7 @@ export interface CmCdAdstDstrComboResponseDto { /** Controller public 메서드명·시그니처와 동일 */ export interface CmCdAdstDstrService { findSidoCombo(): Promise; + findUserSidoCombo(): Promise; findGugunCombo(sido: string): Promise; + findUserGugunCombo(sido: string): Promise; } diff --git a/apps/as/src/entities/attach/api/attach-service.ts b/apps/as/src/entities/attach/api/attach-service.ts index 0af0a916d..ceba5de3f 100644 --- a/apps/as/src/entities/attach/api/attach-service.ts +++ b/apps/as/src/entities/attach/api/attach-service.ts @@ -16,7 +16,14 @@ async function downloadAttachmentByAtflCd(atflCd: string): Promise { + return await client.get(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID}/${atflId}`, { + responseType: "blob", + }); +} + export const attachService = { deleteAttachmentByAtflId, downloadAttachmentByAtflCd, + downloadAttachmentByAtflId, } satisfies AttachService; diff --git a/apps/as/src/entities/attach/api/queries.ts b/apps/as/src/entities/attach/api/queries.ts index ed9efefbf..09a1ab1d9 100644 --- a/apps/as/src/entities/attach/api/queries.ts +++ b/apps/as/src/entities/attach/api/queries.ts @@ -1,4 +1,5 @@ import { useMutation } from "@tanstack/react-query"; +import type { AxiosResponse } from "axios"; import { ENDPOINT } from "@/shared/config"; @@ -8,6 +9,22 @@ import { attachService } from "./attach-service"; const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const; const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const; +const attachDownloadByAtflIdKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID] as const; + +function triggerBlobDownload(response: AxiosResponse) { + const filename = parseFilenameFromResponse(response); + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); +} export function useDeleteAttachment() { return useMutation({ @@ -21,19 +38,17 @@ export function useDownloadAttachmentByAtflCd() { mutationKey: [...attachDownloadBaseKey, "download"], mutationFn: async (atflCd: string) => { const response = await attachService.downloadAttachmentByAtflCd(atflCd); - const filename = parseFilenameFromResponse(response); - - const blob = new Blob([response.data]); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - - window.URL.revokeObjectURL(url); - document.body.removeChild(link); + triggerBlobDownload(response); + }, + }); +} + +export function useDownloadAttachmentByAtflId() { + return useMutation({ + mutationKey: [...attachDownloadByAtflIdKey, "download"], + mutationFn: async (atflId: string) => { + const response = await attachService.downloadAttachmentByAtflId(atflId); + triggerBlobDownload(response); }, }); } diff --git a/apps/as/src/entities/attach/index.ts b/apps/as/src/entities/attach/index.ts index 37258e6e5..c2c771b43 100644 --- a/apps/as/src/entities/attach/index.ts +++ b/apps/as/src/entities/attach/index.ts @@ -1,3 +1,7 @@ export { attachService } from "./api/attach-service"; -export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries"; +export { + useDeleteAttachment, + useDownloadAttachmentByAtflCd, + useDownloadAttachmentByAtflId, +} from "./api/queries"; export type { Attach, AttachService } from "./model/attach-interface"; diff --git a/apps/as/src/entities/attach/model/attach-interface.ts b/apps/as/src/entities/attach/model/attach-interface.ts index d6b0a08d2..f603a328e 100644 --- a/apps/as/src/entities/attach/model/attach-interface.ts +++ b/apps/as/src/entities/attach/model/attach-interface.ts @@ -19,4 +19,5 @@ export interface Attach { export interface AttachService { deleteAttachmentByAtflId: (atflId: string) => Promise; downloadAttachmentByAtflCd: (atflCd: string) => Promise; + downloadAttachmentByAtflId: (atflId: string) => Promise; } diff --git a/apps/as/src/entities/auth/api/auth-service.ts b/apps/as/src/entities/auth/api/auth-service.ts index 0c1730054..d84f84776 100644 --- a/apps/as/src/entities/auth/api/auth-service.ts +++ b/apps/as/src/entities/auth/api/auth-service.ts @@ -1,13 +1,20 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config"; -import { getRemainingMs } from "@common/auth"; +import { + type AuthPayload, + type AuthService, + clearDeviceId, + getOrCreateDeviceId, +} from "@common/auth"; -import { clearAuthToken, setToken, setupRefreshTimer } from "../lib/token-manager"; -import type { AuthPayload, AuthService } from "../model/auth-interface"; +import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager"; async function login(payload: AuthPayload): Promise { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-Device-Id": getOrCreateDeviceId(), + }, body: JSON.stringify(payload), }); @@ -23,10 +30,10 @@ async function login(payload: AuthPayload): Promise { setToken(CLAIM_NAME, accessToken); setToken(AUTH_USER_ID, tokenId); - const remainingMs = getRemainingMs(accessToken); - if (remainingMs > 0) { - setupRefreshTimer(silentRefresh, remainingMs); - } + // const remainingMs = getRemainingMs(accessToken); + // if (remainingMs > 0) { + // setupRefreshTimer(silentRefresh, remainingMs); + // } } async function silentRefresh(): Promise { @@ -34,6 +41,9 @@ async function silentRefresh(): Promise { try { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, { method: "PUT", + headers: { + "X-Device-Id": getOrCreateDeviceId(), + }, }); const data = await response.json(); @@ -52,13 +62,13 @@ async function silentRefresh(): Promise { setToken(CLAIM_NAME, accessToken); setToken(AUTH_USER_ID, tokenId); - const remainingMs = getRemainingMs(accessToken); - if (remainingMs > 0) { - setupRefreshTimer(silentRefresh, remainingMs); - } + // const remainingMs = getRemainingMs(accessToken); + // if (remainingMs > 0) { + // setupRefreshTimer(silentRefresh, remainingMs); + // } } catch (error) { console.error(`[silentRefresh] Failed at ${new Date().toISOString()}`, error); - clearAuthToken(); + tokenManager.clearAuthToken(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); } @@ -66,7 +76,10 @@ async function silentRefresh(): Promise { } async function logout(): Promise { - clearAuthToken(); + await fetch("/api/auth/logout", { method: "POST" }); + tokenManager.clearAuthToken(); + clearDeviceId(); + if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); } @@ -75,5 +88,6 @@ async function logout(): Promise { export const authService: AuthService = { login, silentRefresh, + declineSessionExtend: clearRefreshTimer, logout, }; diff --git a/apps/as/src/entities/auth/lib/token-manager.ts b/apps/as/src/entities/auth/lib/token-manager.ts index d449e711b..78d24bd5e 100644 --- a/apps/as/src/entities/auth/lib/token-manager.ts +++ b/apps/as/src/entities/auth/lib/token-manager.ts @@ -8,7 +8,7 @@ export const { setToken, removeToken, setupRefreshTimer, - clearAuthToken, + clearRefreshTimer, getAccessToken, getRefreshToken, getUserId, diff --git a/apps/as/src/entities/auth/model/auth-interface.ts b/apps/as/src/entities/auth/model/auth-interface.ts deleted file mode 100644 index fb6e10630..000000000 --- a/apps/as/src/entities/auth/model/auth-interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface AuthPayload { - userId: string; - password: string; -} - -export interface AuthService { - login(payload: AuthPayload): Promise; - silentRefresh(): Promise; - logout(): Promise; -} diff --git a/apps/as/src/entities/bns-if/api/bns-if-service.ts b/apps/as/src/entities/bns-if/api/bns-if-service.ts index 320a01efc..316ea041c 100644 --- a/apps/as/src/entities/bns-if/api/bns-if-service.ts +++ b/apps/as/src/entities/bns-if/api/bns-if-service.ts @@ -8,6 +8,8 @@ import type { BnsListResponseWrapperDto, BnsSearchDto, EmCodeListResponseDto, + FacilityListResponseWrapperDto, + FacilitySearchDto, } from "../model/bns-if-interface"; const BASE = ENDPOINT.AS_SERVICE.BNS_IF; @@ -40,8 +42,18 @@ async function getEmCodeList( return response.data; } +async function getFacilityList( + params: FacilitySearchDto, +): Promise { + const response = await client.get(`${BASE}/facilityList`, { + params, + }); + return response.data; +} + export const bnsIfService = { getBnsList, getBnsDetail, getEmCodeList, + getFacilityList, } satisfies BnsIfService; diff --git a/apps/as/src/entities/bns-if/api/queries.ts b/apps/as/src/entities/bns-if/api/queries.ts index 7ab0b46a3..8edf93b0c 100644 --- a/apps/as/src/entities/bns-if/api/queries.ts +++ b/apps/as/src/entities/bns-if/api/queries.ts @@ -2,7 +2,7 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import type { BnsSearchDto } from "../model/bns-if-interface"; +import type { BnsSearchDto, FacilitySearchDto } from "../model/bns-if-interface"; import { bnsIfService } from "./bns-if-service"; @@ -31,3 +31,12 @@ export function useEmCodeList(dpmCd?: string, lclgvrnmntCd?: string) { queryFn: () => bnsIfService.getEmCodeList(dpmCd, lclgvrnmntCd), }); } + +export function useFacilityList(params: FacilitySearchDto, options?: { enabled?: boolean }) { + return useQuery({ + queryKey: [...bnsIfBaseKey, "getFacilityList", params], + queryFn: () => bnsIfService.getFacilityList(params), + enabled: options?.enabled, + placeholderData: keepPreviousData, + }); +} diff --git a/apps/as/src/entities/bns-if/index.ts b/apps/as/src/entities/bns-if/index.ts index 430f4a611..d01b27189 100644 --- a/apps/as/src/entities/bns-if/index.ts +++ b/apps/as/src/entities/bns-if/index.ts @@ -1,5 +1,5 @@ export { bnsIfService } from "./api/bns-if-service"; -export { useBnsDetail, useBnsList, useEmCodeList } from "./api/queries"; +export { useBnsDetail, useBnsList, useEmCodeList, useFacilityList } from "./api/queries"; export { buildBnsSearchDto } from "./lib/build-bns-search-dto"; export type { BnsDetailResponseDto, @@ -10,4 +10,7 @@ export type { BnsSearchDto, EmCodeItemDto, EmCodeListResponseDto, + FacilityListResponseWrapperDto, + FacilityResponseDto, + FacilitySearchDto, } from "./model/bns-if-interface"; diff --git a/apps/as/src/entities/bns-if/model/bns-if-interface.ts b/apps/as/src/entities/bns-if/model/bns-if-interface.ts index e13ab8500..833accca8 100644 --- a/apps/as/src/entities/bns-if/model/bns-if-interface.ts +++ b/apps/as/src/entities/bns-if/model/bns-if-interface.ts @@ -117,9 +117,89 @@ export interface EmCodeListResponseDto { ddtlList: EmCodeItemDto[]; } +/** 백엔드 FacilitySearchDto */ +export interface FacilitySearchDto { + sido?: string; + gugun?: string; +} + +/** 백엔드 FacilityResponseDto */ +export interface FacilityResponseDto { + bscFctCd?: string; + bscFctNm?: string; + addr?: string; + fctCpc?: string; + fctCpc1?: string; + fctCpc2?: string; + fctCpc3?: string; + totInflAmntAve?: unknown; + pInflAmntAve?: unknown; + oInflAmntAve?: unknown; + aInflAmntAve?: unknown; + totTputAve?: unknown; + pTputAve?: unknown; + oTputAve?: unknown; + aTputAve?: unknown; + inflAssctBfrWqltBod?: unknown; + efflWqltBod?: unknown; + inflAssctBfrWqltCod?: unknown; + efflWqltCod?: unknown; + inflAssctBfrWqltSs?: unknown; + efflWqltSs?: unknown; + inflAssctBfrWqltTn?: unknown; + efflWqltTn?: unknown; + inflAssctBfrWqltTp?: unknown; + efflWqltTp?: unknown; + inflAssctBfrWqltClfgrpcnt?: unknown; + efflWqltClfgrpcnt?: unknown; + tmEfficiency?: unknown; + pcLoad?: unknown; + trtFrfHanNm?: string; + applctNtcheNm?: string; + excretionsTot?: unknown; + stkrsngWstwtrTot?: unknown; + lchtTot?: unknown; + etcTot?: unknown; + tel?: string; + rnDt?: string; + compDt?: string; + tot?: unknown; + oprClsNm?: string; + trstOprEtpNm?: string; + yrstpTrstCst?: string; + trstOprDt?: string; + trstAfr?: string; + dsfctNm?: string; + rvNm?: string; + mnstr?: string; + wbsnNm?: string; + efflWqltStndrdNm?: string; + elcpwAmnt?: unknown; + toeRt?: unknown; + toeSum?: unknown; + co2Emissions?: unknown; + toeQnty?: unknown; + toeRt2?: unknown; + useQnty?: unknown; + eqpPrmsDt?: string; + mdmbsn?: string; + lgbsn?: string; + lgbsnBod?: string; + cntrTrtFctNm?: string; + totWtrQrm?: unknown; + inflAssctBfrWqltToc?: unknown; + efflWqltToc?: unknown; +} + +/** 백엔드 FacilityListResponseWrapperDto */ +export interface FacilityListResponseWrapperDto { + fctList: FacilityResponseDto[]; +} + /** Controller public 메서드명·시그니처와 동일 */ export interface BnsIfService { getBnsList(params: BnsSearchDto): Promise; getBnsDetail(gbn: string, yr: string, seqNo: string): Promise; getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise; + getFacilityList(params: FacilitySearchDto): Promise; } diff --git a/apps/as/src/entities/board/api/post-queries.ts b/apps/as/src/entities/board/api/post-queries.ts new file mode 100644 index 000000000..4cf801e0a --- /dev/null +++ b/apps/as/src/entities/board/api/post-queries.ts @@ -0,0 +1,15 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { UserPostSearchParams } from "../model/post-interface"; + +import { postApi } from "./post-service"; + +export function useUserPostList(params?: UserPostSearchParams) { + return useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params], + queryFn: () => postApi.getUserPostList(params), + refetchOnWindowFocus: false, + }); +} diff --git a/apps/as/src/entities/board/api/post-service.ts b/apps/as/src/entities/board/api/post-service.ts new file mode 100644 index 000000000..9cd25f976 --- /dev/null +++ b/apps/as/src/entities/board/api/post-service.ts @@ -0,0 +1,16 @@ +import { client } from "@/shared/api"; +import { AS_DASHBOARD_BBS_ID, ENDPOINT } from "@/shared/config"; + +import { PostService, UserPostListResponse, UserPostSearchParams } from "../model/post-interface"; + +async function getUserPostList(params?: UserPostSearchParams): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/user/${AS_DASHBOARD_BBS_ID}`, + { params }, + ); + return response.data; +} + +export const postApi: PostService = { + getUserPostList, +}; diff --git a/apps/as/src/entities/board/index.ts b/apps/as/src/entities/board/index.ts new file mode 100644 index 000000000..2bec23972 --- /dev/null +++ b/apps/as/src/entities/board/index.ts @@ -0,0 +1,8 @@ +export { useUserPostList } from "./api/post-queries"; +export { postApi } from "./api/post-service"; +export type { + PostService, + UserPost, + UserPostListResponse, + UserPostSearchParams, +} from "./model/post-interface"; diff --git a/apps/as/src/entities/board/model/post-interface.ts b/apps/as/src/entities/board/model/post-interface.ts new file mode 100644 index 000000000..31eca4e64 --- /dev/null +++ b/apps/as/src/entities/board/model/post-interface.ts @@ -0,0 +1,40 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +export interface UserPostSearchParams extends PageableParams { + keywordType?: string; + keyword?: string; + popupYn?: string; + upndFixYn?: string; + prdPstgYn?: string; + siteId?: string; + bbsTypeCd?: string; + ansUseYn?: string; +} + +export interface UserPost { + rowNo: number; + bbsId: number; + pstNo: number; + pstTtl: string; + pstCn: string; + inqNocs: number; + rfrncUrl: string; + popupYn: string; + upndFixYn: string; + prdPstgYn: string; + rootPath: string; + pstPath: string; + ansPstn: number; + wrtrNm: string; + wrtrCplc: string; + atflCd: string; + regDt: string; + rgtrNm: string; + cmntCnt: number; +} + +export type UserPostListResponse = PageResponse; + +export interface PostService { + getUserPostList(params?: UserPostSearchParams): Promise; +} diff --git a/apps/as/src/entities/cnstp/model/cnstp-interface.ts b/apps/as/src/entities/cnstp/model/cnstp-interface.ts index def16901a..eabd12f46 100644 --- a/apps/as/src/entities/cnstp/model/cnstp-interface.ts +++ b/apps/as/src/entities/cnstp/model/cnstp-interface.ts @@ -16,6 +16,7 @@ export interface CnstpSeMngResponseDto { cdLvNo: number; useYn: string; expln?: string; + sortSeq?: number; rgtr?: string; rgtrNm?: string; mdfr?: string; @@ -39,6 +40,7 @@ export interface CnstpSeMngSaveRequestDto { cdLvNo?: number; useYn?: string; expln?: string; + sortSeq?: number; } /** PUT /{cnstpSeCd} 요청 (mode는 서비스에서 U로 설정) */ diff --git a/apps/as/src/entities/code/ui/code-select.tsx b/apps/as/src/entities/code/ui/code-select.tsx index e803e9285..74d011bcd 100644 --- a/apps/as/src/entities/code/ui/code-select.tsx +++ b/apps/as/src/entities/code/ui/code-select.tsx @@ -18,13 +18,7 @@ interface CodeSelectProps extends React.ComponentPropsWithoutRef className?: string; } -function CodeSelect({ - upCd, - useYn, - placeholder = "선택", - className, - ...props -}: CodeSelectProps) { +function CodeSelect({ upCd, useYn, placeholder = "선택", className, ...props }: CodeSelectProps) { const listQuery = useCodeList(useYn ? undefined : upCd); const listByUseYnQuery = useCodeListByUseYn(useYn ? upCd : undefined, useYn ?? ""); @@ -34,7 +28,7 @@ function CodeSelect({ return (