[as] feat: 행정지원 업무 신청 시 시도/구군 선택 로직 추가

This commit is contained in:
2026-07-31 13:50:07 +09:00
parent e0956e05a6
commit 2acbeb83d0
26 changed files with 460 additions and 177 deletions
@@ -2,11 +2,26 @@ import { DesignRegistPage } from "@/pages/design-cnstn";
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
interface PageProps {
searchParams: Promise<{ taskSeComCd?: string }>;
searchParams: Promise<{
taskSeComCd?: string;
admdstCd?: string;
sido?: string;
gugun?: string;
}>;
}
export default async function Page({ searchParams }: PageProps) {
const params = await searchParams;
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
return <DesignRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
return (
<DesignRegistPage
defaults={{
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
admdstCd: params.admdstCd,
sido: params.sido,
gugun: params.gugun,
}}
/>
);
}
@@ -2,11 +2,26 @@ import { FnrsRegistPage } from "@/pages/fnrs-cnsltn";
import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema";
interface PageProps {
searchParams: Promise<{ taskSeComCd?: string }>;
searchParams: Promise<{
taskSeComCd?: string;
admdstCd?: string;
sido?: string;
gugun?: string;
}>;
}
export default async function Page({ searchParams }: PageProps) {
const params = await searchParams;
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
return <FnrsRegistPage taskSeComCd={taskSeComCd.length ? taskSeComCd : undefined} />;
return (
<FnrsRegistPage
defaults={{
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
admdstCd: params.admdstCd,
sido: params.sido,
gugun: params.gugun,
}}
/>
);
}
@@ -1,7 +1,12 @@
import { InstlRegistPage } from "@/pages/instl-sncn";
interface InstlSncnRegistRouteProps {
searchParams: Promise<{ taskSeComCd?: string }>;
searchParams: Promise<{
taskSeComCd?: string;
admdstCd?: string;
sido?: string;
gugun?: string;
}>;
}
export default async function Page({ searchParams }: InstlSncnRegistRouteProps) {
@@ -10,5 +15,14 @@ export default async function Page({ searchParams }: InstlSncnRegistRouteProps)
? params.taskSeComCd.split(",").filter(Boolean)
: undefined;
return <InstlRegistPage taskSeComCd={taskSeComCd} />;
return (
<InstlRegistPage
defaults={{
taskSeComCd,
admdstCd: params.admdstCd,
sido: params.sido,
gugun: params.gugun,
}}
/>
);
}
@@ -13,6 +13,11 @@ async function findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]> {
return response.data;
}
async function findUserSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]> {
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/user/sido`);
return response.data;
}
async function findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]> {
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/gugun`, {
params: { sido },
@@ -20,7 +25,16 @@ async function findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDt
return response.data;
}
async function findUserGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]> {
const response = await client.get<CmCdAdstDstrComboResponseDto[]>(`${BASE}/user/gugun`, {
params: { sido },
});
return response.data;
}
export const cmCdAdstDstrService = {
findSidoCombo,
findUserSidoCombo,
findGugunCombo,
findUserGugunCombo,
} satisfies CmCdAdstDstrService;
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { IUserInfo } from "@/shared/store";
import { cmCdAdstDstrService } from "./adstdstr-service";
@@ -13,6 +14,16 @@ export function useFindSidoCombo() {
});
}
export function useFindUserSidoCombo(userInfo?: IUserInfo | null) {
const adstDstrCdByUser = userInfo?.laeSeCd?.substring(2, 4);
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findUserSidoCombo", adstDstrCdByUser ?? "all"],
queryFn: () => cmCdAdstDstrService.findUserSidoCombo(),
select: (data) =>
adstDstrCdByUser ? data.filter((item) => item.adstDstrCd === adstDstrCdByUser) : data,
});
}
export function useFindGugunCombo(sido: string) {
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findGugunCombo", sido],
@@ -20,3 +31,11 @@ export function useFindGugunCombo(sido: string) {
enabled: Boolean(sido),
});
}
export function useFindUserGugunCombo(sido: string) {
return useQuery({
queryKey: [...cmCdAdstDstrBaseKey, "findUserGugunCombo", sido],
queryFn: () => cmCdAdstDstrService.findUserGugunCombo(sido),
enabled: Boolean(sido),
});
}
+6 -1
View File
@@ -1,5 +1,10 @@
export { cmCdAdstDstrService } from "./api/adstdstr-service";
export { useFindGugunCombo, useFindSidoCombo } from "./api/queries";
export {
useFindGugunCombo,
useFindSidoCombo,
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "./api/queries";
export type {
CmCdAdstDstrComboResponseDto,
CmCdAdstDstrService,
@@ -7,5 +7,7 @@ export interface CmCdAdstDstrComboResponseDto {
/** Controller public 메서드명·시그니처와 동일 */
export interface CmCdAdstDstrService {
findSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]>;
findUserSidoCombo(): Promise<CmCdAdstDstrComboResponseDto[]>;
findGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]>;
findUserGugunCombo(sido: string): Promise<CmCdAdstDstrComboResponseDto[]>;
}
@@ -70,7 +70,12 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
export function getCreateDefaultValues(
userInfo: IUserInfo | null,
taskSeComCd?: string[],
options?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
},
): DesignCnstnRegistFormData {
return {
designCnstnMng: {
@@ -78,7 +83,7 @@ export function getCreateDefaultValues(
bizBgngYr: "",
bizEndYr: "",
bizCn: "",
taskSeComCd: taskSeComCd ?? [],
taskSeComCd: options?.taskSeComCd ?? [],
dsgnrNm: "",
dsgnrTelno: "",
dsgnrOgdpNm: "",
@@ -87,6 +92,9 @@ export function getCreateDefaultValues(
aplcntMblTelno: userInfo?.mphn ?? "",
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
aplcntEmlAddr: userInfo?.email ?? "",
admdstCd: options?.admdstCd ?? "",
sido: options?.sido ?? "",
gugun: options?.gugun ?? "",
prgrsSttsComCd: "CV",
atchFileGroupId: "",
},
@@ -36,10 +36,15 @@ interface DesignFormPageProps {
id?: string;
mode: "create" | "edit";
initialData?: DesignCnstnMngDetResponseDto;
taskSeComCd?: string[];
createDefaults?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
};
}
function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPageProps) {
function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPageProps) {
const queryClient = useQueryClient();
const router = useRouter();
const { confirm } = useConfirm();
@@ -54,7 +59,7 @@ function DesignFormPage({ id, mode, initialData, taskSeComCd }: DesignFormPagePr
defaultValues:
isEditMode && initialData
? mapDetailToFormData(initialData)
: getCreateDefaultValues(userInfo, taskSeComCd),
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
@@ -11,7 +11,7 @@ import {
type DesignCnstnMngSearchDto,
useDesignCnstnMngPage,
} from "@/entities/design-cnstn";
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { formatTaskSeComCdForApi } from "../lib/design-cnstn-regist-schema";
@@ -71,9 +71,15 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
},
});
const handleConfirmTaskSe = (selectedCodes: string[]) => {
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
setIsTaskSeDialogOpen(false);
router.push(`/design-cnstn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
const qs = new URLSearchParams({
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
admdstCd: selection.admdstCd,
sido: selection.sido,
gugun: selection.gugun,
});
router.push(`/design-cnstn/regist?${qs}`);
};
return (
@@ -87,10 +93,10 @@ function DesignList({ searchParams, pagination, onPaginationChange }: DesignList
</div>
{tableComponent}
</section>
<TaskSePickerDialog
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
onConfirm={handleConfirmTaskSe}
onConfirm={handleConfirm}
title="설계자문 신청"
codeUpCd="CNFM"
/>
@@ -1,11 +1,19 @@
import { DesignFormPage } from "./design-form-page";
interface DesignRegistPageProps {
interface DesignRegistDefaults {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
}
function DesignRegistPage({ taskSeComCd }: DesignRegistPageProps) {
return <DesignFormPage mode="create" taskSeComCd={taskSeComCd} />;
interface DesignRegistPageProps {
defaults?: DesignRegistDefaults;
}
function DesignRegistPage({ defaults }: DesignRegistPageProps) {
return <DesignFormPage mode="create" createDefaults={defaults} />;
}
export { DesignRegistPage };
export type { DesignRegistDefaults };
@@ -3,7 +3,6 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
@@ -21,10 +20,12 @@ import {
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
function BasicInfoFormSection() {
const userInfo = useUserStore((state) => state.userInfo);
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
.filter(Boolean)
.join(" ");
const { data: taskOptions } = useCodeList("CNFM");
function handleBnsSelect(data: BnsApplyData) {
@@ -142,7 +143,7 @@ function BasicInfoFormSection() {
<TableHead data-required>구분</TableHead>
<TableCell>{taskSeComCdLabel}</TableCell>
<TableHead>신청 지자체</TableHead>
<TableCell>{userInfo?.instSeNm}</TableCell>
<TableCell>{localGovLabel}</TableCell>
</TableRow>
</TableHeader>
</Table>
@@ -118,7 +118,12 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
export function getCreateDefaultValues(
userInfo: IUserInfo | null,
taskSeComCd?: string[],
options?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
},
): FnrsCnsltnRegistFormData {
return {
fnrsCnsltnMng: {
@@ -126,7 +131,7 @@ export function getCreateDefaultValues(
bizBgngYr: "",
bizEndYr: "",
bizCn: "",
taskSeComCd: taskSeComCd ?? [],
taskSeComCd: options?.taskSeComCd ?? [],
cfmtnBgtBizNo: "",
bgtAplyYr: "",
dsgnrNm: "",
@@ -137,6 +142,9 @@ export function getCreateDefaultValues(
aplcntMblTelno: userInfo?.mphn ?? "",
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
aplcntEmlAddr: userInfo?.email ?? "",
admdstCd: options?.admdstCd ?? "",
sido: options?.sido ?? "",
gugun: options?.gugun ?? "",
prgrsSttsComCd: "SV",
atchFileGroupId: "",
sharAtchFileGroupId: "",
@@ -47,10 +47,15 @@ interface FnrsFormPageProps {
id?: string;
mode: "create" | "edit";
initialData?: FnrsCnsltnMngDetResponseDto;
taskSeComCd?: string[];
createDefaults?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
};
}
function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps) {
function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPageProps) {
const queryClient = useQueryClient();
const router = useRouter();
const { confirm } = useConfirm();
@@ -73,7 +78,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
defaultValues:
isEditMode && initialData
? mapDetailToFormData(initialData)
: getCreateDefaultValues(userInfo, taskSeComCd),
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
+11 -5
View File
@@ -11,7 +11,7 @@ import {
type FnrsCnsltnMngSearchDto,
useFnrsCnsltnMngPage,
} from "@/entities/fnrs-cnsltn";
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { formatTaskSeComCdForApi } from "../lib/fnrs-cnsltn-regist-schema";
@@ -71,9 +71,15 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
},
});
const handleConfirmTaskSe = (selectedCodes: string[]) => {
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
setIsTaskSeDialogOpen(false);
router.push(`/fnrs-cnsltn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
const qs = new URLSearchParams({
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
admdstCd: selection.admdstCd,
sido: selection.sido,
gugun: selection.gugun,
});
router.push(`/fnrs-cnsltn/regist?${qs}`);
};
return (
@@ -87,10 +93,10 @@ function FnrsList({ searchParams, pagination, onPaginationChange }: FnrsListProp
</div>
{tableComponent}
</section>
<TaskSePickerDialog
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
onConfirm={handleConfirmTaskSe}
onConfirm={handleConfirm}
title="재원협의 신청"
codeUpCd="FNRS"
/>
@@ -1,11 +1,19 @@
import { FnrsFormPage } from "./fnrs-form-page";
interface FnrsRegistPageProps {
interface FnrsRegistDefaults {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
}
function FnrsRegistPage({ taskSeComCd }: FnrsRegistPageProps) {
return <FnrsFormPage mode="create" taskSeComCd={taskSeComCd} />;
interface FnrsRegistPageProps {
defaults?: FnrsRegistDefaults;
}
function FnrsRegistPage({ defaults }: FnrsRegistPageProps) {
return <FnrsFormPage mode="create" createDefaults={defaults} />;
}
export { FnrsRegistPage };
export type { FnrsRegistDefaults };
@@ -3,7 +3,6 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
@@ -24,10 +23,12 @@ import {
import type { FnrsCnsltnRegistFormData } from "../../lib/fnrs-cnsltn-regist-schema";
function BasicInfoFormSection() {
const userInfo = useUserStore((state) => state.userInfo);
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
.filter(Boolean)
.join(" ");
const { data: taskOptions } = useCodeList("FNRS");
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
@@ -48,6 +49,12 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="35%" />
<col width="15%" />
<col width="35%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[20rem]" data-required>
@@ -141,7 +148,7 @@ function BasicInfoFormSection() {
<TableHead data-required>구분</TableHead>
<TableCell>{taskSeComCdLabel}</TableCell>
<TableHead>신청 지자체</TableHead>
<TableCell>{userInfo?.instSeNm}</TableCell>
<TableCell>{localGovLabel}</TableCell>
</TableRow>
{taskSeComCd.includes("RE") && (
<>
@@ -108,7 +108,12 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
export function getCreateDefaultValues(
userInfo: IUserInfo | null,
taskSeComCd?: string[],
options?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
},
): InstlSncnRegistFormData {
return {
instlSncnMng: {
@@ -116,7 +121,7 @@ export function getCreateDefaultValues(
bizBgngYr: "",
bizEndYr: "",
bizCn: "",
taskSeComCd: taskSeComCd ?? [],
taskSeComCd: options?.taskSeComCd ?? [],
sncnSeComCd: "",
dsgnrNm: "",
dsgnrTelno: "",
@@ -126,6 +131,9 @@ export function getCreateDefaultValues(
aplcntMblTelno: userInfo?.mphn ?? "",
aplcntOgdpNm: userInfo?.ogdpSeNm ?? "",
aplcntEmlAddr: userInfo?.email ?? "",
admdstCd: options?.admdstCd ?? "",
sido: options?.sido ?? "",
gugun: options?.gugun ?? "",
prgrsSttsComCd: "SV",
atchFileGroupId: "",
},
@@ -39,10 +39,15 @@ interface InstlFormPageProps {
id?: string;
mode: "create" | "edit";
initialData?: InstlSncnMngDetResponse;
taskSeComCd?: string[];
createDefaults?: {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
};
}
function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProps) {
function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageProps) {
const queryClient = useQueryClient();
const router = useRouter();
const { confirm } = useConfirm();
@@ -58,7 +63,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
defaultValues:
isEditMode && initialData
? mapDetailToFormData(initialData)
: getCreateDefaultValues(userInfo, taskSeComCd),
: getCreateDefaultValues(userInfo, createDefaults),
});
function buildFormPayload(
+11 -5
View File
@@ -11,7 +11,7 @@ import {
type InstlSncnMngSearchParams,
useInstlSncnList,
} from "@/entities/instl-sncn";
import { TaskSePickerDialog } from "@/widgets/pbadms-sprt";
import { PbadmsTaskApplyDialog, type PbadmsTaskApplySelection } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { formatTaskSeComCdForApi } from "../lib/instl-sncn-regist-schema";
@@ -75,9 +75,15 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
setIsTaskSeDialogOpen(true);
};
const handleConfirmTaskSe = (selectedCodes: string[]) => {
const handleConfirm = (selection: PbadmsTaskApplySelection) => {
setIsTaskSeDialogOpen(false);
router.push(`/instl-sncn/regist?taskSeComCd=${formatTaskSeComCdForApi(selectedCodes)}`);
const qs = new URLSearchParams({
taskSeComCd: formatTaskSeComCdForApi(selection.selectedCodes),
admdstCd: selection.admdstCd,
sido: selection.sido,
gugun: selection.gugun,
});
router.push(`/instl-sncn/regist?${qs}`);
};
return (
@@ -91,10 +97,10 @@ function InstlList({ searchParams, pagination, onPaginationChange }: InstlListPr
</div>
{tableComponent}
</section>
<TaskSePickerDialog
<PbadmsTaskApplyDialog
open={isTaskSeDialogOpen}
onOpenChange={setIsTaskSeDialogOpen}
onConfirm={handleConfirmTaskSe}
onConfirm={handleConfirm}
title="설치인가 신청"
codeUpCd="INSTL"
/>
@@ -1,11 +1,19 @@
import { InstlFormPage } from "./instl-form-page";
interface InstlRegistPageProps {
interface InstlRegistDefaults {
taskSeComCd?: string[];
admdstCd?: string;
sido?: string;
gugun?: string;
}
function InstlRegistPage({ taskSeComCd }: InstlRegistPageProps) {
return <InstlFormPage mode="create" taskSeComCd={taskSeComCd} />;
interface InstlRegistPageProps {
defaults?: InstlRegistDefaults;
}
function InstlRegistPage({ defaults }: InstlRegistPageProps) {
return <InstlFormPage mode="create" createDefaults={defaults} />;
}
export { InstlRegistPage };
export type { InstlRegistDefaults };
@@ -3,7 +3,6 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
@@ -24,10 +23,12 @@ import {
import type { InstlSncnRegistFormData } from "../../lib/instl-sncn-regist-schema";
function BasicInfoFormSection() {
const userInfo = useUserStore((state) => state.userInfo);
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
.filter(Boolean)
.join(" ");
function handleBnsSelect(data: BnsApplyData) {
setValue("instlSncnMng.bizNm", data.bizNm);
@@ -52,6 +53,12 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="35%" />
<col width="15%" />
<col width="35%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[20rem]" data-required>
@@ -145,7 +152,7 @@ function BasicInfoFormSection() {
<TableHead data-required>구분</TableHead>
<TableCell>{taskSeComCdLabel}</TableCell>
<TableHead>신청 지자체</TableHead>
<TableCell>{userInfo?.instSeNm}</TableCell>
<TableCell>{localGovLabel}</TableCell>
</TableRow>
<TableRow>
<TableHead data-required>사업구분</TableHead>
@@ -2,11 +2,12 @@
import { useEffect, useState } from "react";
import { useUserStore } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
type CmCdAdstDstrComboResponseDto,
useFindGugunCombo,
useFindSidoCombo,
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "@/entities/adstdstr";
import { useCodeList } from "@/entities/code";
import {
@@ -50,13 +51,19 @@ interface SewerBscPlanApplyDialogProps {
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlanApplyDialogProps) {
const userInfo = useUserStore((state) => state.userInfo);
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 sidoFilterUserInfo =
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
? userInfo
: undefined;
const { data: sidoData, isPending: isSidoPending } = useFindUserSidoCombo(sidoFilterUserInfo);
const { data: gugunData, isPending: isGugunPending } = useFindUserGugunCombo(sidoComCd);
const { data: planSeOptions, isPending: isPlanSePending } = useCodeList("SEWERBSCPLANSE");
const { data: taskSeOptions, isPending: isTaskSePending } = useCodeList("SEWER");
+4 -1
View File
@@ -33,6 +33,10 @@ export { BusinessScaleFormSection } from "./ui/business-scale-form-section";
export { ChecklistFormSection } from "./ui/checklist-form-section";
export { ContactInfoFormSection } from "./ui/contact-info-form-section";
export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section";
export {
PbadmsTaskApplyDialog,
type PbadmsTaskApplySelection,
} from "./ui/pbadms-task-apply-dialog";
export { PrivacyConsentSection } from "./ui/privacy-consent-section";
export { SectionCollapsible } from "./ui/section-collapsible";
export {
@@ -42,4 +46,3 @@ export {
} from "./ui/splmnt-dmnd-dialog";
export { type SplmntDmndFormItem, SplmntDmndFormSection } from "./ui/splmnt-dmnd-form-section";
export { SupplementDemandSection } from "./ui/supplement-demand-section";
export { TaskSePickerDialog } from "./ui/task-se-picker-dialog";
@@ -0,0 +1,210 @@
"use client";
import { useEffect, useState } from "react";
import { useUserStore } from "@/shared/lib";
import { Field, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import {
type CmCdAdstDstrComboResponseDto,
useFindUserGugunCombo,
useFindUserSidoCombo,
} from "@/entities/adstdstr";
import { useCodeList } from "@/entities/code";
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Skeleton,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
interface PbadmsTaskApplySelection {
admdstCd: string;
sido: string;
gugun: string;
selectedCodes: string[];
}
interface PbadmsTaskApplyDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (selection: PbadmsTaskApplySelection) => void;
title: string;
codeUpCd?: string;
}
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
function PbadmsTaskApplyDialog({
open,
onOpenChange,
onConfirm,
title,
codeUpCd,
}: PbadmsTaskApplyDialogProps) {
const userInfo = useUserStore((state) => state.userInfo);
const [sidoComCd, setSidoComCd] = useState("");
const [sggComCd, setSggComCd] = useState("");
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const sidoFilterUserInfo =
userInfo?.dtlInstCd === "ROLE_USER04" || userInfo?.dtlInstCd === "ROLE_USER05"
? userInfo
: undefined;
const { data: sidoData, isPending: isSidoPending } = useFindUserSidoCombo(sidoFilterUserInfo);
const { data: gugunData, isPending: isGugunPending } = useFindUserGugunCombo(sidoComCd);
const { data: taskOptions, isPending: isTaskSePending } = useCodeList(codeUpCd);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
const isPending = isSidoPending || isTaskSePending;
const canConfirm = Boolean(sidoComCd && selectedCodes.length > 0);
useEffect(() => {
if (!open) return;
setSidoComCd("");
setSggComCd("");
setSelectedCodes([]);
}, [open]);
function handleCheckedChange(code: string, checked: boolean) {
setSelectedCodes((prev) => {
if (!checked) return prev.filter((value) => value !== code);
if (code.includes("RE")) return [code];
return [...prev.filter((value) => !value.includes("RE")), code];
});
}
function handleConfirm() {
if (!canConfirm) return;
const sidoOption = sidoOptions.find((option) => option.adstDstrCd === sidoComCd);
const gugunOption = gugunOptions.find((option) => option.adstDstrCd === sggComCd);
onConfirm({
admdstCd: sggComCd || sidoComCd,
sido: sidoOption?.adstDstrNm ?? "",
gugun: gugunOption?.adstDstrNm ?? "",
selectedCodes,
});
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{title}</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">
<FieldGroup data-slot="checkbox-group" className="flex-row flex-wrap">
{taskOptions?.map((option) => (
<Field key={option.lwpoCd} orientation="horizontal" className="w-auto">
<Checkbox
id={`pbadms-task-apply-${option.lwpoCd}`}
checked={selectedCodes.includes(option.lwpoCd)}
onCheckedChange={(checked) =>
handleCheckedChange(option.lwpoCd, checked === true)
}
/>
<FieldLabel
htmlFor={`pbadms-task-apply-${option.lwpoCd}`}
className="font-normal"
>
{option.lwpoCdNm}
</FieldLabel>
</Field>
))}
</FieldGroup>
</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 { PbadmsTaskApplyDialog };
export type { PbadmsTaskApplySelection };
@@ -1,117 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { Field, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { useCodeList } from "@/entities/code";
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Skeleton,
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
interface TaskSePickerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (selectedCodes: string[]) => void;
title: string;
codeUpCd?: string;
}
function TaskSePickerDialog({
open,
onOpenChange,
onConfirm,
title,
codeUpCd,
}: TaskSePickerDialogProps) {
const [selectedCodes, setSelectedCodes] = useState<string[]>([]);
const { data: taskOptions, isPending } = useCodeList(codeUpCd);
useEffect(() => {
if (!open) return;
setSelectedCodes([]);
}, [open]);
function handleCheckedChange(code: string, checked: boolean) {
setSelectedCodes((prev) => {
if (!checked) return prev.filter((value) => value !== code);
if (code.includes("RE")) return [code];
return [...prev.filter((value) => !value.includes("RE")), code];
});
}
function handleConfirm() {
onConfirm(selectedCodes);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
{isPending ? (
<Skeleton className="h-[16rem] w-full" />
) : (
<Table variant="secondary">
<TableHeader>
<TableRow>
<TableHead>업무구분</TableHead>
<TableCell>
<FieldSet className="px-2">
<FieldGroup data-slot="checkbox-group" className="flex-row flex-wrap">
{taskOptions?.map((option) => (
<Field key={option.lwpoCd} orientation="horizontal" className="w-auto">
<Checkbox
id={`task-se-picker-${option.lwpoCd}`}
checked={selectedCodes.includes(option.lwpoCd)}
onCheckedChange={(checked) =>
handleCheckedChange(option.lwpoCd, checked === true)
}
/>
<FieldLabel
htmlFor={`task-se-picker-${option.lwpoCd}`}
className="font-normal"
>
{option.lwpoCdNm}
</FieldLabel>
</Field>
))}
</FieldGroup>
</FieldSet>
</TableCell>
</TableRow>
</TableHeader>
</Table>
)}
<DialogFooter>
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
취소
</Button>
<Button
type="button"
onClick={handleConfirm}
disabled={isPending || selectedCodes.length === 0}
>
확인
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { TaskSePickerDialog };