From afbebb9dfc6725c6aceec50bd821f17e18a936d3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 29 Jul 2026 10:24:38 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=ED=95=98=EC=88=98=EB=8F=84?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sewer-bsc-plan/page.tsx | 13 +- .../sewer-bsc-plan/regist/page.tsx | 18 +- .../src/entities/bns-if/api/bns-if-service.ts | 12 + apps/as/src/entities/bns-if/api/queries.ts | 11 +- apps/as/src/entities/bns-if/index.ts | 5 +- .../entities/bns-if/model/bns-if-interface.ts | 80 ++++++ apps/as/src/pages/sewer-bsc-plan/index.ts | 1 + .../sewer-bsc-plan/lib/form-data-mapper.ts | 52 ++-- .../lib/sewer-bsc-plan-regist-schema.ts | 13 +- .../pages/sewer-bsc-plan/model/columns.tsx | 202 +++++++++++++++ .../ui/sections/basic-info-form-section.tsx | 30 +-- .../ui/sections/fnrs-cst-form-section.tsx | 2 +- .../ui/sections/outline-form-section.tsx | 2 +- .../ui/sections/tmplnt-form-section.tsx | 74 +++--- .../ui/sewer-bsc-plan-apply-dialog.tsx | 229 ++++++++++++++++++ .../ui/sewer-bsc-plan-form-page.tsx | 30 ++- .../ui/sewer-bsc-plan-list-page.tsx | 172 +++++++++++++ .../sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx | 114 +++++++++ .../ui/sewer-bsc-plan-regist-page.tsx | 19 +- apps/as/src/widgets/pbadms-sprt/index.ts | 1 + .../pbadms-sprt/lib/checklist-form-utils.ts | 11 + .../pbadms-sprt/ui/checklist-form-section.tsx | 9 +- 22 files changed, 985 insertions(+), 115 deletions(-) create mode 100644 apps/as/src/pages/sewer-bsc-plan/model/columns.tsx create mode 100644 apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-apply-dialog.tsx create mode 100644 apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page.tsx create mode 100644 apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx diff --git a/apps/as/app/(with-base-layout)/sewer-bsc-plan/page.tsx b/apps/as/app/(with-base-layout)/sewer-bsc-plan/page.tsx index 47f03c736..d07ca4b43 100644 --- a/apps/as/app/(with-base-layout)/sewer-bsc-plan/page.tsx +++ b/apps/as/app/(with-base-layout)/sewer-bsc-plan/page.tsx @@ -1,12 +1,3 @@ -import Link from "next/link"; +import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan"; -export default function Page() { - return ( -
- 하수도기본계획 목록은 후속 구현 예정입니다.{" "} - - 등록 화면으로 이동 - -
- ); -} +export default SewerBscPlanListPage; diff --git a/apps/as/app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx b/apps/as/app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx index c85bd720b..276160546 100644 --- a/apps/as/app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx +++ b/apps/as/app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx @@ -1,5 +1,19 @@ import { SewerBscPlanRegistPage } from "@/pages/sewer-bsc-plan"; -export default function Page() { - return ; +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/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/pages/sewer-bsc-plan/index.ts b/apps/as/src/pages/sewer-bsc-plan/index.ts index d5af2c203..39f824683 100644 --- a/apps/as/src/pages/sewer-bsc-plan/index.ts +++ b/apps/as/src/pages/sewer-bsc-plan/index.ts @@ -1,3 +1,4 @@ export { SewerBscPlanEditPage } from "./ui/sewer-bsc-plan-edit-page"; export { SewerBscPlanFormPage } from "./ui/sewer-bsc-plan-form-page"; +export { SewerBscPlanListPage } from "./ui/sewer-bsc-plan-list-page"; export { SewerBscPlanRegistPage } from "./ui/sewer-bsc-plan-regist-page"; diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts index b24acd348..53c4e3f10 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts @@ -6,32 +6,16 @@ import type { import type { BizCstMatrixItem } from "./biz-cst-matrix-utils"; import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils"; -import { - formatTaskSeComCdForApi, - parseTaskSeComCdFromApi, - type SewerBscPlanRegistFormData, -} from "./sewer-bsc-plan-regist-schema"; +import { type SewerBscPlanRegistFormData } from "./sewer-bsc-plan-regist-schema"; const PLAN_STEPS = [1, 2, 3, 4] as const; function createEmptyOtlnList() { - return PLAN_STEPS.map((planStpNo) => ({ - planStpNo, - sewerPlanPpltnCnt: 0, - sewerPrcsPpltnCnt: 0, - sewerPsplyRt: 0, - })); + return PLAN_STEPS.map((planStpNo) => ({ planStpNo })); } function createEmptyFnrsCstList() { - return PLAN_STEPS.map((planStpNo) => ({ - planStpNo, - gvexpAmt: 0, - lcexpAmt: 0, - orgprBdchgAmt: 0, - etcAmt: 0, - sumAmt: 0, - })); + return PLAN_STEPS.map((planStpNo) => ({ planStpNo })); } function mapSelectedValue(value?: string | null): "Y" | "N" | null { @@ -72,17 +56,29 @@ function createEmptyTmplntDtlList(swtrmtFcltCd: string) { })); } -export function getCreateDefaultValues(userInfo: IUserInfo | null): SewerBscPlanRegistFormData { +export function getCreateDefaultValues( + userInfo: IUserInfo | null, + options?: { + taskSeComCd?: string; + taskSeCdNm?: string; + sewerBscPlanSeComCd?: string; + sewerBscPlanSeCdNm?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }, +): SewerBscPlanRegistFormData { return { sewerBscPlan: { sewerBscPlanNm: "", - sewerBscPlanSeComCd: "", - sewerBscPlanSeCdNm: "", - taskSeComCd: [], - admdstCd: userInfo?.instSeCd ?? "", + sewerBscPlanSeComCd: options?.sewerBscPlanSeComCd ?? "", + sewerBscPlanSeCdNm: options?.sewerBscPlanSeCdNm ?? "", + taskSeComCd: options?.taskSeComCd ?? "", + taskSeCdNm: options?.taskSeCdNm ?? "", + admdstCd: options?.admdstCd ?? userInfo?.instSeCd ?? "", prgrsSttsComCd: "SV", - sido: "", - gugun: userInfo?.instSeNm ?? "", + sido: options?.sido ?? "", + gugun: options?.gugun ?? userInfo?.instSeNm ?? "", }, sewerBscPlanDtl: { planYr: "", @@ -127,7 +123,7 @@ export function mapDetailToFormData(data: SewerBscPlanDetResponseDto): SewerBscP sewerBscPlanNm: detDto.sewerBscPlanNm ?? "", sewerBscPlanSeComCd: detDto.sewerBscPlanSeComCd ?? "", sewerBscPlanSeCdNm: detDto.sewerBscPlanSeCdNm ?? "", - taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd), + taskSeComCd: detDto.taskSeComCd ?? "", admdstCd: detDto.admdstCd, envoSeComCd: detDto.envoSeComCd, prgrsSttsComCd: detDto.prgrsSttsComCd, @@ -250,7 +246,7 @@ export function buildApplyPayload( return { sewerBscPlan: { ...sewerBscPlanRest, - taskSeComCd: formatTaskSeComCdForApi(taskSeComCd), + taskSeComCd: taskSeComCd ?? "", sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined, prgrsSttsComCd, }, diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts index 168463a52..e49524a31 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts @@ -23,7 +23,8 @@ export const sewerBscPlanRegistSchema = z.object({ sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"), sewerBscPlanSeComCd: z.string().optional(), sewerBscPlanSeCdNm: z.string().optional(), - taskSeComCd: z.array(z.string()).optional(), + taskSeComCd: z.string().optional(), + taskSeCdNm: z.string().optional(), admdstCd: z.string().optional(), envoSeComCd: z.string().optional(), prgrsSttsComCd: z.string().optional(), @@ -129,15 +130,5 @@ export const sewerBscPlanRegistSchema = z.object({ export type SewerBscPlanRegistFormData = z.infer; -export function formatTaskSeComCdForApi(taskSeComCd?: string[]): string | undefined { - if (!taskSeComCd?.length) return undefined; - return taskSeComCd.join(","); -} - -export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] { - if (!taskSeComCd) return []; - return taskSeComCd.split(",").filter(Boolean); -} - /** 체크리스트·필수첨부 docSeComCd — CMS 합의 전 임시값 */ export const SEWER_BSC_PLAN_DOC_SE_COM_CD = "SEWER"; diff --git a/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx new file mode 100644 index 000000000..4a9b03a9a --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { Fragment } from "react"; + +import { createColumnHelper } from "@tanstack/react-table"; + +import { dayjs } from "@/shared/lib"; +import type { SewerBscPlanResponseDto } from "@/entities/sewer-bsc-plan"; +import { Badge } from "@common/components"; + +const columnHelper = createColumnHelper(); + +const MAIN_STEP_LABELS = { + save: "저장", + apply: "신청완료", + envoReview: "환경청 검토", + techReview: "기술검토", + reply: "검토결과 회신", + supplement: "보완요청", +} as const; + +const TECH_REVIEW_STATUS_CODES = new Set(["EE", "ER", "TE", "TS", "TR", "TP"]); + +type BadgeState = "completed" | "current" | "pending" | "action"; + +interface ProgressBadge { + label: string; + state: BadgeState; +} + +function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): ProgressBadge[] { + const techReviewLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.techReview; + const replyLabel = prgrsSttsCdNm || MAIN_STEP_LABELS.reply; + + if (prgrsSttsComCd === "SV") { + return [ + { label: MAIN_STEP_LABELS.save, state: "current" }, + { label: MAIN_STEP_LABELS.apply, state: "pending" }, + { label: MAIN_STEP_LABELS.envoReview, state: "pending" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "ST") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "action" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "LE") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.reply, state: "action" }, + ]; + } + + if (prgrsSttsComCd === "SL") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "pending" }, + { label: MAIN_STEP_LABELS.supplement, state: "action" }, + ]; + } + + if (TECH_REVIEW_STATUS_CODES.has(prgrsSttsComCd)) { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: techReviewLabel, state: "action" }, + { label: MAIN_STEP_LABELS.reply, state: "pending" }, + ]; + } + + if (prgrsSttsComCd === "EP") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { label: replyLabel, state: "action" }, + ]; + } + + if (prgrsSttsComCd === "TA" || prgrsSttsComCd === "TB") { + return [ + { label: MAIN_STEP_LABELS.save, state: "completed" }, + { label: MAIN_STEP_LABELS.apply, state: "completed" }, + { label: MAIN_STEP_LABELS.envoReview, state: "completed" }, + { label: MAIN_STEP_LABELS.techReview, state: "completed" }, + { label: MAIN_STEP_LABELS.reply, state: "completed" }, + ]; + } + + return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; +} + +function getBadgeColor(state: BadgeState) { + switch (state) { + case "completed": + return "green"; + case "current": + return "blue"; + case "pending": + return "lightGray"; + case "action": + return "orange"; + } +} + +function formatOptional(value: string | undefined) { + return value?.trim() ? value : "-"; +} + +interface ProgressStepBadgeProps { + prgrsSttsComCd: string; + prgrsSttsCdNm: string; +} + +function ProgressStepBadge({ prgrsSttsComCd, prgrsSttsCdNm }: ProgressStepBadgeProps) { + const badges = buildProgressBadges(prgrsSttsComCd, prgrsSttsCdNm); + + return ( +
+ {badges.map((badge, index) => ( + + {index > 0 && >} + {badge.label} + + ))} +
+ ); +} + +export function createSewerBscPlanColumns() { + return [ + columnHelper.accessor("sido", { + header: "시도", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("gugun", { + header: "시군구", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("sewerBscPlanNm", { + header: "기본계획명", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("sewerBscPlanSeCdNm", { + header: "계획구분", + meta: { align: "center" }, + cell: ({ row }) => + formatOptional(row.original.sewerBscPlanSeCdNm || row.original.sewerBscPlanSeComCd), + }), + columnHelper.accessor("taskSeCdNm", { + header: "업무구분", + meta: { align: "center" }, + cell: ({ row }) => formatOptional(row.original.taskSeCdNm || row.original.taskSeComCd), + }), + columnHelper.accessor("rgtrNm", { + header: "담당자", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.accessor("envoSeCdNm", { + header: "해당기관", + meta: { align: "center" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + columnHelper.display({ + id: "progress", + header: "진행상황", + meta: { align: "center" }, + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("regDt", { + header: "등록일", + meta: { align: "center" }, + cell: ({ getValue }) => { + const value = getValue(); + return value ? dayjs(value).format("YYYY-MM-DD") : "-"; + }, + }), + ]; +} diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sections/basic-info-form-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sections/basic-info-form-section.tsx index 20be550c8..c935b9185 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sections/basic-info-form-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sections/basic-info-form-section.tsx @@ -2,7 +2,6 @@ import { Controller, useFormContext } from "react-hook-form"; -import { useUserStore } from "@/shared/lib"; import { Field, FieldError } from "@/shared/ui/field"; import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { @@ -24,15 +23,18 @@ import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options"; function BasicInfoFormSection() { - const userInfo = useUserStore((state) => state.userInfo); const { control, setValue, watch } = useFormContext(); const planYearOptions = getPlanYearOptions(); const goalYearOptions = getGoalYearOptions(); - const seCdNm = watch("sewerBscPlan.sewerBscPlanSeCdNm"); - const localGovLabel = - [watch("sewerBscPlan.sido"), watch("sewerBscPlan.gugun")].filter(Boolean).join(" ") || - userInfo?.instSeNm || - "-"; + const sewerBscPlanSeLabel = [ + watch("sewerBscPlan.sewerBscPlanSeCdNm"), + watch("sewerBscPlan.taskSeCdNm"), + ] + .filter(Boolean) + .join(" "); + const localGovLabel = [watch("sewerBscPlan.sido"), watch("sewerBscPlan.gugun")] + .filter(Boolean) + .join(" "); function handleFrlvlGoalYrChange(value: string) { setValue("sewerBscPlanDtl.frlvlGoalYr", value, { shouldDirty: true, shouldValidate: true }); @@ -93,7 +95,7 @@ function BasicInfoFormSection() { render={({ field, fieldState }) => ( - + @@ -146,7 +148,7 @@ function BasicInfoFormSection() { value={field.value || undefined} onValueChange={handleFrlvlGoalYrChange} > - + @@ -170,7 +172,7 @@ function BasicInfoFormSection() { render={({ field, fieldState }) => ( - + @@ -220,7 +222,7 @@ function BasicInfoFormSection() { render={({ field, fieldState }) => ( { + setSidoComCd(value); + setSggComCd(""); + }} + > + + + + + {sidoOptions.map((option) => ( + + {option.adstDstrNm} + + ))} + + + + 시군구 + + + + + + 계획구분 + +
+ +
+ {planSeOptions?.map((option) => ( + + + + {option.lwpoCdNm} + + + ))} +
+
+
+
+
+ + 업무구분 + +
+ +
+ {taskSeOptions?.map((option) => ( + + + + {option.lwpoCdNm} + + + ))} +
+
+
+
+
+ + + )} + + + + + + + + ); +} + +export { SewerBscPlanApplyDialog }; +export type { SewerBscPlanApplySelection }; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx index 7b1ed3412..d08fc6eec 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx @@ -27,7 +27,11 @@ import { import { Button } from "@common/components"; import { validateFnrsCstBalance } from "../lib/fnrs-cst-form-utils"; -import { buildApplyPayload, getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper"; +import { + buildApplyPayload, + getCreateDefaultValues, + mapDetailToFormData, +} from "../lib/form-data-mapper"; import { SEWER_BSC_PLAN_DOC_SE_COM_CD, type SewerBscPlanRegistFormData, @@ -44,9 +48,23 @@ interface SewerBscPlanFormPageProps { id?: string; mode: "create" | "edit"; initialData?: SewerBscPlanDetResponseDto; + createDefaults?: { + taskSeComCd?: string; + taskSeCdNm?: string; + sewerBscPlanSeComCd?: string; + sewerBscPlanSeCdNm?: string; + admdstCd?: string; + sido?: string; + gugun?: string; + }; } -function SewerBscPlanFormPage({ id, mode, initialData }: SewerBscPlanFormPageProps) { +function SewerBscPlanFormPage({ + id, + mode, + initialData, + createDefaults, +}: SewerBscPlanFormPageProps) { const queryClient = useQueryClient(); const router = useRouter(); const { confirm } = useConfirm(); @@ -65,15 +83,11 @@ function SewerBscPlanFormPage({ id, mode, initialData }: SewerBscPlanFormPagePro defaultValues: isEditMode && initialData ? mapDetailToFormData(initialData) - : getCreateDefaultValues(userInfo), + : getCreateDefaultValues(userInfo, createDefaults), }); function assertFnrsCstBalance(data: SewerBscPlanRegistFormData): boolean { - const validationMessage = validateFnrsCstBalance( - data.bizCstList, - data.fnrsCstList, - costItems, - ); + const validationMessage = validateFnrsCstBalance(data.bizCstList, data.fnrsCstList, costItems); if (validationMessage) { alert(validationMessage); return false; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page.tsx new file mode 100644 index 000000000..8f4f64c8c --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list-page.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useState } from "react"; + +import type { PaginationState } from "@tanstack/react-table"; + +import { + type CmCdAdstDstrComboResponseDto, + useFindGugunCombo, + useFindSidoCombo, +} from "@/entities/adstdstr"; +import { CodeSelect } from "@/entities/code"; +import type { SewerBscPlanSearchRequestDto } from "@/entities/sewer-bsc-plan"; +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +import { SewerBscPlanList } from "./sewer-bsc-plan-list"; + +const DEFAULT_PAGE_INDEX = 0; +const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_SORT = "sewerBscPlanDocNo,desc"; +const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = []; + +const DEFAULT_SEARCH_PARAMS: SewerBscPlanSearchRequestDto = { + sewerBscPlanNm: "", + sewerBscPlanSeComCd: "", + taskSeComCd: "", + admdstCd: "", + page: DEFAULT_PAGE_INDEX, + size: DEFAULT_PAGE_SIZE, + sort: DEFAULT_SORT, +}; + +const ALL_SELECT_VALUE = "__ALL__"; + +function SewerBscPlanListPage() { + const [sewerBscPlanNm, setSewerBscPlanNm] = useState(""); + const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState(""); + const [taskSeComCd, setTaskSeComCd] = useState(""); + const [sidoComCd, setSidoComCd] = useState(""); + const [sggComCd, setSggComCd] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: DEFAULT_PAGE_INDEX, + pageSize: DEFAULT_PAGE_SIZE, + }); + const [searchParams, setSearchParams] = + useState(DEFAULT_SEARCH_PARAMS); + + const { data: sidoData } = useFindSidoCombo(); + const { data: gugunData } = useFindGugunCombo(sidoComCd); + const sidoOptions = sidoData ?? EMPTY_ADST_LIST; + const gugunOptions = gugunData ?? EMPTY_ADST_LIST; + + const handleSearch = (event: React.FormEvent) => { + event.preventDefault(); + setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_PAGE_INDEX })); + setSearchParams({ + ...DEFAULT_SEARCH_PARAMS, + sewerBscPlanNm, + sewerBscPlanSeComCd, + taskSeComCd, + admdstCd: sggComCd || sidoComCd || "", + page: DEFAULT_PAGE_INDEX, + size: pagination.pageSize, + }); + }; + + return ( +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + setSewerBscPlanNm(event.target.value)} + placeholder="기본계획명" + /> +
+ +
+
+ + +
+ ); +} + +export { SewerBscPlanListPage }; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx new file mode 100644 index 000000000..9bab87255 --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-list.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; + +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; + +import { useDataTable, useUserStore } from "@/shared/lib"; +import { + type SewerBscPlanResponseDto, + type SewerBscPlanSearchRequestDto, + useSewerBscPlanPage, +} from "@/entities/sewer-bsc-plan"; +import { Button } from "@common/components"; + +import { createSewerBscPlanColumns } from "../model/columns"; + +import { + SewerBscPlanApplyDialog, + type SewerBscPlanApplySelection, +} from "./sewer-bsc-plan-apply-dialog"; + +interface SewerBscPlanListProps { + searchParams: SewerBscPlanSearchRequestDto; + pagination: PaginationState; + onPaginationChange: (pagination: PaginationState) => void; +} + +function SewerBscPlanList({ searchParams, pagination, onPaginationChange }: SewerBscPlanListProps) { + const userInfo = useUserStore((state) => state.userInfo); + const router = useRouter(); + const [isApplyDialogOpen, setIsApplyDialogOpen] = useState(false); + + const queryParams = useMemo( + () => ({ + sewerBscPlanNm: searchParams.sewerBscPlanNm, + sewerBscPlanSeComCd: searchParams.sewerBscPlanSeComCd, + taskSeComCd: searchParams.taskSeComCd, + admdstCd: searchParams.admdstCd, + page: pagination.pageIndex, + size: pagination.pageSize, + sort: searchParams.sort ?? "sewerBscPlanDocNo,desc", + }), + [searchParams, pagination], + ); + + const pageQuery = useSewerBscPlanPage(queryParams); + const columns = useMemo(() => createSewerBscPlanColumns(), []); + + const handleRowClick = (row: SewerBscPlanResponseDto) => { + const canEdit = + (row.prgrsSttsComCd === "SV" || row.prgrsSttsComCd === "SL") && + row.rgtr === userInfo?.userUnqId; + + router.push( + canEdit + ? `/sewer-bsc-plan/${row.sewerBscPlanDocNo}/edit` + : `/sewer-bsc-plan/${row.sewerBscPlanDocNo}`, + ); + }; + + const { tableComponent } = useDataTable({ + query: pageQuery, + tableOptions: { + columns, + state: { pagination }, + onPaginationChange: (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + onPaginationChange(next); + }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }, + dataTableOptions: { + viewPagination: true, + onRowClick: handleRowClick, + }, + }); + + function handleConfirmApply(selection: SewerBscPlanApplySelection) { + setIsApplyDialogOpen(false); + const qs = new URLSearchParams({ + taskSeComCd: selection.taskSeComCd, + taskSeCdNm: selection.taskSeCdNm, + sewerBscPlanSeComCd: selection.sewerBscPlanSeComCd, + sewerBscPlanSeCdNm: selection.sewerBscPlanSeCdNm, + admdstCd: selection.admdstCd, + sido: selection.sido, + gugun: selection.gugun, + }); + router.push(`/sewer-bsc-plan/regist?${qs}`); + } + + return ( + <> +
+
+

하수도기본계획 목록

+ +
+ {tableComponent} +
+ + + ); +} + +export { SewerBscPlanList }; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx index c55fc1363..23609e666 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx @@ -1,7 +1,22 @@ import { SewerBscPlanFormPage } from "./sewer-bsc-plan-form-page"; -function SewerBscPlanRegistPage() { - return ; +interface SewerBscPlanRegistDefaults { + taskSeComCd?: string; + taskSeCdNm?: string; + sewerBscPlanSeComCd?: string; + sewerBscPlanSeCdNm?: string; + admdstCd?: string; + sido?: string; + gugun?: string; +} + +interface SewerBscPlanRegistPageProps { + defaults?: SewerBscPlanRegistDefaults; +} + +function SewerBscPlanRegistPage({ defaults }: SewerBscPlanRegistPageProps) { + return ; } export { SewerBscPlanRegistPage }; +export type { SewerBscPlanRegistDefaults }; diff --git a/apps/as/src/widgets/pbadms-sprt/index.ts b/apps/as/src/widgets/pbadms-sprt/index.ts index 7cec9e8b0..9ace89ed1 100644 --- a/apps/as/src/widgets/pbadms-sprt/index.ts +++ b/apps/as/src/widgets/pbadms-sprt/index.ts @@ -13,6 +13,7 @@ export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-s export { buildChecklistTabs, isFormChecklistComplete, + normalizeTaskSeComCdArray, syncCklstResultListFromTree, } from "./lib/checklist-form-utils"; export type { FnrsSharItemDef, FnrsSharListItem } from "./lib/fnrs-shar-form-utils"; diff --git a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts index 07f015648..fbf63472d 100644 --- a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts +++ b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts @@ -26,6 +26,17 @@ interface CklstResultListItem { picRvwCn?: string | null; } +export function normalizeTaskSeComCdArray( + value: string | string[] | undefined | null, +): string[] { + if (!value) return []; + if (Array.isArray(value)) return value.filter(Boolean); + return value + .split(",") + .map((code) => code.trim()) + .filter(Boolean); +} + function groupCklstTreeByTaskSeComCd(cklstTree: CklstTreeNode[]): Map { const grouped = new Map(); diff --git a/apps/as/src/widgets/pbadms-sprt/ui/checklist-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/checklist-form-section.tsx index c4857f066..572c9f2c3 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/checklist-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/checklist-form-section.tsx @@ -25,7 +25,7 @@ import { TabsTrigger, } from "@common/components"; -import { buildChecklistTabs, syncCklstResultListFromTree } from "../lib/checklist-form-utils"; +import { buildChecklistTabs, normalizeTaskSeComCdArray, syncCklstResultListFromTree } from "../lib/checklist-form-utils"; import { SectionCollapsible } from "./section-collapsible"; @@ -48,8 +48,11 @@ interface ChecklistFormSectionProps { function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionProps) { const { watch, setValue, control, getValues } = useFormContext(); const [activeTab, setActiveTab] = useState(""); - const taskSeComCd = watch(`${fieldPrefix}.taskSeComCd`) as string[] | undefined; - const taskSeComCdArray = useMemo(() => taskSeComCd ?? [], [taskSeComCd]); + const taskSeComCd = watch(`${fieldPrefix}.taskSeComCd`) as string | string[] | undefined; + const taskSeComCdArray = useMemo( + () => normalizeTaskSeComCdArray(taskSeComCd), + [taskSeComCd], + ); const cklstResultList = (useWatch({ control, name: "cklstResultList" }) as CklstResultListItem[] | undefined) ?? [];