[as] feat: 하수도기본계획 목록 페이지 구현

This commit is contained in:
2026-07-29 10:24:38 +09:00
parent fe76864a05
commit afbebb9dfc
22 changed files with 985 additions and 115 deletions
@@ -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;
+10 -1
View File
@@ -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,
});
}
+4 -1
View File
@@ -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">&gt;</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 };
+1
View File
@@ -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) ?? [];