[as] feat: 하수도기본계획 목록 페이지 구현
This commit is contained in:
@@ -1,12 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="mt-6 px-4 text-[1.4rem]">
|
||||
하수도기본계획 목록은 후속 구현 예정입니다.{" "}
|
||||
<Link className="underline" href="/sewer-bsc-plan/regist">
|
||||
등록 화면으로 이동
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default SewerBscPlanListPage;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { SewerBscPlanRegistPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default function Page() {
|
||||
return <SewerBscPlanRegistPage />;
|
||||
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 <SewerBscPlanRegistPage defaults={params} />;
|
||||
}
|
||||
|
||||
@@ -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<FacilityListResponseWrapperDto> {
|
||||
const response = await client.get<FacilityListResponseWrapperDto>(`${BASE}/facilityList`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const bnsIfService = {
|
||||
getBnsList,
|
||||
getBnsDetail,
|
||||
getEmCodeList,
|
||||
getFacilityList,
|
||||
} satisfies BnsIfService;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<BnsListResponseWrapperDto>;
|
||||
getBnsDetail(gbn: string, yr: string, seqNo: string): Promise<BnsDetailResponseWrapperDto>;
|
||||
getEmCodeList(dpmCd?: string, lclgvrnmntCd?: string): Promise<EmCodeListResponseDto>;
|
||||
getFacilityList(params: FacilitySearchDto): Promise<FacilityListResponseWrapperDto>;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<typeof sewerBscPlanRegistSchema>;
|
||||
|
||||
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";
|
||||
|
||||
@@ -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<SewerBscPlanResponseDto>();
|
||||
|
||||
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 (
|
||||
<div className="flex flex-wrap items-center justify-start">
|
||||
{badges.map((badge, index) => (
|
||||
<Fragment key={`${badge.label}-${index}`}>
|
||||
{index > 0 && <span className="mx-2">></span>}
|
||||
<Badge color={getBadgeColor(badge.state)}>{badge.label}</Badge>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) => (
|
||||
<ProgressStepBadge
|
||||
prgrsSttsComCd={row.original.prgrsSttsComCd || ""}
|
||||
prgrsSttsCdNm={row.original.prgrsSttsCdNm || ""}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일",
|
||||
meta: { align: "center" },
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? dayjs(value).format("YYYY-MM-DD") : "-";
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -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<SewerBscPlanRegistFormData>();
|
||||
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 }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -117,7 +119,7 @@ function BasicInfoFormSection() {
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -146,7 +148,7 @@ function BasicInfoFormSection() {
|
||||
value={field.value || undefined}
|
||||
onValueChange={handleFrlvlGoalYrChange}
|
||||
>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -170,7 +172,7 @@ function BasicInfoFormSection() {
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -196,7 +198,7 @@ function BasicInfoFormSection() {
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -220,7 +222,7 @@ function BasicInfoFormSection() {
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Select value={field.value || undefined} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="w-[16rem]">
|
||||
<SelectTrigger aria-invalid={fieldState.invalid} className="!w-[16rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -261,7 +263,7 @@ function BasicInfoFormSection() {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableCell>{seCdNm || "-"}</TableCell>
|
||||
<TableCell>{sewerBscPlanSeLabel}</TableCell>
|
||||
<TableHead>신청 지자체</TableHead>
|
||||
<TableCell>{localGovLabel}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -158,7 +158,7 @@ function FnrsCstFormSection() {
|
||||
className={cn("text-right", row.readonly && "bg-muted")}
|
||||
readOnly={row.readonly}
|
||||
disabled={row.readonly}
|
||||
value={item?.[row.key] ?? 0}
|
||||
value={item?.[row.key] ?? ""}
|
||||
onChange={(e) => {
|
||||
if (row.readonly) return;
|
||||
handleChange(planStpNo, row.key as EditableFnrsKey, e.target.value);
|
||||
|
||||
@@ -91,7 +91,7 @@ function OutlineFormSection() {
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className="text-right"
|
||||
value={item?.[row.key] ?? 0}
|
||||
value={item?.[row.key] ?? ""}
|
||||
onChange={(e) => handleChange(planStpNo, row.key, e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { useFacilityList } from "@/entities/bns-if";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Checkbox,
|
||||
@@ -46,15 +46,6 @@ const TMPLNT_FIELD_ROWS = [
|
||||
{ key: "exlnPumpCnt" as const, label: "우수 펌프(개소)" },
|
||||
];
|
||||
|
||||
/** 시설 마스터 API TBD — faker mock */
|
||||
function createMockFacilities(): FacilityOption[] {
|
||||
faker.seed(20260727);
|
||||
return Array.from({ length: 6 }, (_, index) => ({
|
||||
code: `FCLT${String(index + 1).padStart(3, "0")}`,
|
||||
name: `${faker.location.city()} 공공하수처리시설`,
|
||||
}));
|
||||
}
|
||||
|
||||
function TmplntFormSection() {
|
||||
const { confirm } = useConfirm();
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
@@ -63,12 +54,25 @@ function TmplntFormSection() {
|
||||
name: "sewerBscPlanTmplntList",
|
||||
});
|
||||
const tmplntList = useWatch({ control, name: "sewerBscPlanTmplntList" }) ?? [];
|
||||
const sido = useWatch({ control, name: "sewerBscPlan.sido" }) ?? "";
|
||||
const gugun = useWatch({ control, name: "sewerBscPlan.gugun" }) ?? "";
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
|
||||
const facilityOptions = useMemo(() => createMockFacilities(), []);
|
||||
const { data: facilityListData } = useFacilityList({ sido, gugun }, { enabled: Boolean(sido) });
|
||||
|
||||
const facilityOptions = useMemo(
|
||||
() =>
|
||||
(facilityListData?.fctList ?? [])
|
||||
.filter((facility) => facility.bscFctCd)
|
||||
.map((facility) => ({
|
||||
code: facility.bscFctCd!,
|
||||
name: facility.bscFctNm ?? facility.bscFctCd!,
|
||||
})),
|
||||
[facilityListData?.fctList],
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState("");
|
||||
|
||||
const selectedCodes = useMemo(
|
||||
@@ -128,34 +132,36 @@ function TmplntFormSection() {
|
||||
}
|
||||
|
||||
const resolvedActiveTab =
|
||||
activeTab && selectedCodes.has(activeTab)
|
||||
? activeTab
|
||||
: (tmplntList[0]?.swtrmtFcltCd ?? "");
|
||||
activeTab && selectedCodes.has(activeTab) ? activeTab : (tmplntList[0]?.swtrmtFcltCd ?? "");
|
||||
|
||||
return (
|
||||
<SectionCollapsible title="처리시설별 하수도정비기본계획">
|
||||
<div className="space-y-4 p-2">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{facilityOptions.map((option) => (
|
||||
<label key={option.code} className="flex items-center gap-2 text-xl">
|
||||
<Checkbox
|
||||
checked={selectedCodes.has(option.code)}
|
||||
onCheckedChange={(checked) => handleFacilityToggle(option, checked === true)}
|
||||
/>
|
||||
<span>{option.name}</span>
|
||||
</label>
|
||||
))}
|
||||
{facilityOptions.length === 0 ? (
|
||||
<p className="text-xl text-muted-foreground">조회된 처리시설이 없습니다.</p>
|
||||
) : (
|
||||
facilityOptions.map((option) => (
|
||||
<label key={option.code} className="flex items-center gap-2 text-xl">
|
||||
<Checkbox
|
||||
checked={selectedCodes.has(option.code)}
|
||||
onCheckedChange={(checked) => handleFacilityToggle(option, checked === true)}
|
||||
/>
|
||||
<span>{option.name}</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fields.length > 0 ? (
|
||||
<Tabs
|
||||
value={resolvedActiveTab}
|
||||
onValueChange={setActiveTab}
|
||||
variant="underline"
|
||||
>
|
||||
<TabsList>
|
||||
<Tabs value={resolvedActiveTab} onValueChange={setActiveTab} variant="underline">
|
||||
<TabsList className="p-0">
|
||||
{fields.map((field) => (
|
||||
<TabsTrigger key={field.id} value={field.swtrmtFcltCd}>
|
||||
<TabsTrigger
|
||||
key={field.id}
|
||||
value={field.swtrmtFcltCd}
|
||||
className="px-6 py-3 text-2xl"
|
||||
>
|
||||
{field.swtrmtFcltNm || field.swtrmtFcltCd}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -164,7 +170,11 @@ function TmplntFormSection() {
|
||||
{fields.map((field, facilityIndex) => {
|
||||
const dtlList = tmplntList[facilityIndex]?.sewerBscPlanTmplntDtlList ?? [];
|
||||
return (
|
||||
<TabsContent key={field.id} value={field.swtrmtFcltCd}>
|
||||
<TabsContent
|
||||
key={field.id}
|
||||
value={field.swtrmtFcltCd}
|
||||
className="mt-0 border-0 p-0 shadow-none"
|
||||
>
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -216,7 +226,7 @@ function TmplntFormSection() {
|
||||
})}
|
||||
</Tabs>
|
||||
) : (
|
||||
<p className="px-2 text-xl text-muted-foreground">
|
||||
<p className="text-xl text-muted-foreground">
|
||||
처리시설을 선택하면 시설별 입력 탭이 표시됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface SewerBscPlanApplySelection {
|
||||
admdstCd: string;
|
||||
sido: string;
|
||||
gugun: string;
|
||||
sewerBscPlanSeComCd: string;
|
||||
sewerBscPlanSeCdNm: string;
|
||||
taskSeComCd: string;
|
||||
taskSeCdNm: string;
|
||||
}
|
||||
|
||||
interface SewerBscPlanApplyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (selection: SewerBscPlanApplySelection) => void;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlanApplyDialogProps) {
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [sewerBscPlanSeComCd, setSewerBscPlanSeComCd] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
const { data: planSeOptions, isPending: isPlanSePending } = useCodeList("SEWERBSCPLANSE");
|
||||
const { data: taskSeOptions, isPending: isTaskSePending } = useCodeList("SEWER");
|
||||
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
const isPending = isSidoPending || isPlanSePending || isTaskSePending;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setSidoComCd("");
|
||||
setSggComCd("");
|
||||
setSewerBscPlanSeComCd("");
|
||||
setTaskSeComCd("");
|
||||
}, [open]);
|
||||
|
||||
const canConfirm = Boolean(sidoComCd && sewerBscPlanSeComCd && taskSeComCd);
|
||||
|
||||
function handleConfirm() {
|
||||
if (!canConfirm) return;
|
||||
|
||||
const sidoOption = sidoOptions.find((option) => option.adstDstrCd === sidoComCd);
|
||||
const gugunOption = gugunOptions.find((option) => option.adstDstrCd === sggComCd);
|
||||
const planSeOption = planSeOptions?.find((option) => option.lwpoCd === sewerBscPlanSeComCd);
|
||||
const taskSeOption = taskSeOptions?.find((option) => option.lwpoCd === taskSeComCd);
|
||||
|
||||
onConfirm({
|
||||
admdstCd: sggComCd || sidoComCd,
|
||||
sido: sidoOption?.adstDstrNm ?? "",
|
||||
gugun: gugunOption?.adstDstrNm ?? "",
|
||||
sewerBscPlanSeComCd,
|
||||
sewerBscPlanSeCdNm: planSeOption?.lwpoCdNm ?? "",
|
||||
taskSeComCd,
|
||||
taskSeCdNm: taskSeOption?.lwpoCdNm ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>하수도기본계획 신청</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isPending ? (
|
||||
<Skeleton className="h-[16rem] w-full" />
|
||||
) : (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
<col width="15%" />
|
||||
<col width="35%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sidoComCd || undefined}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead>시군구</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={setSggComCd}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>계획구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup
|
||||
value={sewerBscPlanSeComCd || undefined}
|
||||
onValueChange={setSewerBscPlanSeComCd}
|
||||
>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{planSeOptions?.map((option) => (
|
||||
<FieldLabel
|
||||
key={option.lwpoCd}
|
||||
htmlFor={`sewer-plan-se-${option.lwpoCd}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem
|
||||
value={option.lwpoCd}
|
||||
id={`sewer-plan-se-${option.lwpoCd}`}
|
||||
/>
|
||||
<span className="font-normal">{option.lwpoCdNm}</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead data-required>업무구분</TableHead>
|
||||
<TableCell colSpan={3}>
|
||||
<FieldSet className="px-2">
|
||||
<RadioGroup value={taskSeComCd || undefined} onValueChange={setTaskSeComCd}>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{taskSeOptions?.map((option) => (
|
||||
<FieldLabel
|
||||
key={option.lwpoCd}
|
||||
htmlFor={`sewer-task-se-${option.lwpoCd}`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field orientation="horizontal" className="w-auto">
|
||||
<RadioGroupItem
|
||||
value={option.lwpoCd}
|
||||
id={`sewer-task-se-${option.lwpoCd}`}
|
||||
/>
|
||||
<span className="font-normal">{option.lwpoCdNm}</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="button" onClick={handleConfirm} disabled={isPending || !canConfirm}>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanApplyDialog };
|
||||
export type { SewerBscPlanApplySelection };
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PaginationState>({
|
||||
pageIndex: DEFAULT_PAGE_INDEX,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<SewerBscPlanSearchRequestDto>(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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="space-y-4 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<form className="flex flex-wrap items-center gap-4 p-2" onSubmit={handleSearch}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sidoComCd" className="whitespace-nowrap">
|
||||
시도
|
||||
</Label>
|
||||
<Select
|
||||
value={sidoComCd}
|
||||
onValueChange={(value) => {
|
||||
setSidoComCd(value === ALL_SELECT_VALUE ? "" : value);
|
||||
setSggComCd("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="sidoComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sggComCd" className="whitespace-nowrap">
|
||||
시군구
|
||||
</Label>
|
||||
<Select
|
||||
value={sggComCd}
|
||||
onValueChange={(value) => setSggComCd(value === ALL_SELECT_VALUE ? "" : value)}
|
||||
disabled={!sidoComCd}
|
||||
>
|
||||
<SelectTrigger id="sggComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_SELECT_VALUE}>전체</SelectItem>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="sewerBscPlanSeComCd" className="whitespace-nowrap">
|
||||
계획구분
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="SEWERBSCPLANSE"
|
||||
value={sewerBscPlanSeComCd}
|
||||
onValueChange={setSewerBscPlanSeComCd}
|
||||
placeholder="전체"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="taskSeComCd" className="whitespace-nowrap">
|
||||
업무구분
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="SEWER"
|
||||
value={taskSeComCd}
|
||||
onValueChange={setTaskSeComCd}
|
||||
placeholder="전체"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-[32rem] items-center gap-2">
|
||||
<Label htmlFor="sewerBscPlanNm" className="whitespace-nowrap">
|
||||
기본계획명
|
||||
</Label>
|
||||
<Input
|
||||
id="sewerBscPlanNm"
|
||||
value={sewerBscPlanNm}
|
||||
onChange={(event) => setSewerBscPlanNm(event.target.value)}
|
||||
placeholder="기본계획명"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">조회</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<SewerBscPlanList
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanListPage };
|
||||
@@ -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 (
|
||||
<>
|
||||
<section className="card card-border mx-0 bg-background pb-8 pt-0">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">하수도기본계획 목록</h3>
|
||||
<Button type="button" color="green" onClick={() => setIsApplyDialogOpen(true)}>
|
||||
신청
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
</section>
|
||||
<SewerBscPlanApplyDialog
|
||||
open={isApplyDialogOpen}
|
||||
onOpenChange={setIsApplyDialogOpen}
|
||||
onConfirm={handleConfirmApply}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { SewerBscPlanList };
|
||||
@@ -1,7 +1,22 @@
|
||||
import { SewerBscPlanFormPage } from "./sewer-bsc-plan-form-page";
|
||||
|
||||
function SewerBscPlanRegistPage() {
|
||||
return <SewerBscPlanFormPage mode="create" />;
|
||||
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 <SewerBscPlanFormPage mode="create" createDefaults={defaults} />;
|
||||
}
|
||||
|
||||
export { SewerBscPlanRegistPage };
|
||||
export type { SewerBscPlanRegistDefaults };
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<string, CklstTreeNode[]> {
|
||||
const grouped = new Map<string, CklstTreeNode[]>();
|
||||
|
||||
|
||||
@@ -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) ?? [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user