[as] feat: 재원협의 보완요청 로직 작업

This commit is contained in:
2026-07-16 17:54:57 +09:00
parent f584e21480
commit a6f9ba7526
14 changed files with 284 additions and 348 deletions
@@ -2,7 +2,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
strg: "strg",
aply: "aply",
del: "del",
splmntCmptn: "splmntCmptn",
fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
@@ -32,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
],
SL: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt],
},
ROLE_USER04: {
@@ -83,6 +83,24 @@ export const fnrsCnsltnRegistSchema = z.object({
atchFileGroupId: z.string(),
}),
),
splmntDmndList: z.array(
z.object({
splmntDmndSn: z.number().optional(),
splmntArtclComCd: z.string().optional(),
splmntArtclCdNm: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
splmntDmndAtchFileGroupId: z.string().optional(),
splmntDmndAtchFileNm: z.string().optional(),
splmntCn: z.string().optional(),
splmntPicNm: z.string().optional(),
splmntCmptnDt: z.string().optional(),
splmntAtchFileGroupId: z.string().optional(),
splmntAtchFileNm: z.string().optional(),
splmntAtchFileAtflId: z.string().optional(),
cmptnYn: z.string().optional(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
required_error: "개인정보 수집/이용 동의는 필수입니다",
}),
@@ -34,6 +34,7 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
const bizCstList = data.bizCstList ?? [];
const cklstResultList = data.cklstResultList;
const esntlFileList = data.esntlFileList;
const splmntDmndList = data.splmntDmndList;
return {
fnrsCnsltnMng: {
@@ -94,6 +95,23 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
picRvwCn: item.picRvwCn,
})) ?? [],
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
splmntDmndList:
splmntDmndList?.map((item) => ({
splmntDmndSn: item.splmntDmndSn,
splmntArtclComCd: item.splmntArtclComCd,
splmntArtclCdNm: item.splmntArtclCdNm,
splmntDmndCn: item.splmntDmndCn,
splmntDmndDt: item.splmntDmndDt,
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId ?? "",
splmntDmndAtchFileNm: item.splmntDmndAtchFileInfo?.orgnlFileNm ?? "",
splmntCn: item.splmntCn ?? "",
splmntPicNm: item.splmntPicNm ?? "",
splmntCmptnDt: item.splmntCmptnDt ?? "",
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
cmptnYn: item.cmptnYn,
})) ?? [],
privacyConsent: "Y",
};
}
@@ -130,6 +148,7 @@ export function getCreateDefaultValues(
fnrsSharList: [],
cklstResultList: [],
esntlAtchFileList: [],
splmntDmndList: [],
privacyConsent: "N",
};
}
@@ -112,8 +112,7 @@ export function buildSupplementReqPayload(
toSplmntDmndSaveItem(item),
);
const nextSn =
existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1;
const newItems: FnrsCnsltnSplmntDmndItem[] = payload.items.map((item, index) => ({
splmntDmndSn: nextSn + index,
@@ -129,7 +128,7 @@ export function buildSupplementReqPayload(
};
}
function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
return {
splmntDmndSn: item.splmntDmndSn,
splmntArtclComCd: item.splmntArtclComCd,
@@ -40,8 +40,8 @@ import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
import {
buildSavePayload,
buildSupplementReqPayload,
buildSupplementSubmitPayload,
buildTechReviewReplyPayload,
toSplmntDmndSaveItem,
} from "../../lib/save-payload-utils";
import { type ChecklistFormState } from "../../model/view-types";
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
@@ -219,15 +219,6 @@ function FloatingActionButtons({
router.push("/fnrs-cnsltn");
}
async function handleSplmntCmptnClick() {
const payload = buildSupplementSubmitPayload(data, checklistFormState);
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn,
request: payload,
successMessage: "보완완료 처리되었습니다.",
});
}
async function handleFnrsCnsltnAncmntClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -380,15 +371,18 @@ function FloatingActionButtons({
return;
}
const payload = buildTechReviewReplyPayload(
data,
checklistFormState,
"KECO",
"TE",
"SV",
getReviewOpinionFormData(),
opnrNm,
);
const payload = {
...buildTechReviewReplyPayload(
data,
checklistFormState,
"KECO",
"TE",
"SV",
getReviewOpinionFormData(),
opnrNm,
),
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
};
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
@@ -439,11 +433,6 @@ function FloatingActionButtons({
삭제
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.splmntCmptn) && (
<Button onClick={handleSplmntCmptnClick} disabled={isProcessing}>
보완완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && (
<Button onClick={handleFnrsCnsltnAncmntClick} disabled={isProcessing}>
재원협의 고시
@@ -518,7 +507,7 @@ function FloatingActionButtons({
open={activeDialog === "dmndSplmnt"}
onOpenChange={() => setActiveDialog(null)}
isSubmitting={isProcessing}
onConfirm={(payload) => void handleDmndSplmntConfirm(payload)}
onConfirm={handleDmndSplmntConfirm}
/>
<ElaprApprovalDialog
@@ -558,19 +547,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
}
}
async function handleSplmntCmptn(
context: ActionHandlerContext<FnrsCnsltnApplySaveRequestDto>,
): Promise<void> {
try {
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
if (!docNo) throw new Error("문서번호가 없습니다.");
await fnrsCnsltnMngService.submitSupplement(docNo, context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleChangePrgrsSttsComCd(
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
): Promise<void> {
@@ -634,7 +610,6 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
strg: handleSaveApply,
aply: handleSaveApply,
del: handleDel,
splmntCmptn: handleSplmntCmptn,
fnrsCnsltnAncmnt: handleChangePrgrsSttsComCd,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleChangePrgrsSttsComCd,
@@ -11,6 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { useAcntgSeMngTree } from "@/entities/acntg";
import {
type FnrsCnsltnApplySaveRequestDto,
fnrsCnsltnMngBaseKey,
type FnrsCnsltnMngDetResponseDto,
fnrsCnsltnMngService,
@@ -28,6 +29,7 @@ import {
isFormChecklistComplete,
PrivacyConsentSection,
recalculateFnrsSharTotals,
SplmntDmndFormSection,
toFnrsSharSaveList,
validateBizCstSharBalance,
} from "@/widgets/pbadms-sprt";
@@ -56,6 +58,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
const alert = useAlertStore((state) => state.setMessage);
const [isSubmitting, setIsSubmitting] = useState(false);
const isEditMode = mode === "edit";
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
@@ -73,10 +76,10 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
: getCreateDefaultValues(userInfo, taskSeComCd),
});
async function submitForm(
function buildFormPayload(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: "SV" | "ST",
): Promise<string | undefined> {
prgrsSttsComCd: "SV" | "ST" | "TE",
): FnrsCnsltnApplySaveRequestDto | undefined {
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems);
const validationMessage = validateBizCstSharBalance(
data.bizCstList,
@@ -86,12 +89,12 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
);
if (validationMessage) {
alert(validationMessage);
return;
return undefined;
}
const { taskSeComCd: formTaskSeComCd, ...fnrsCnsltnMngRest } = data.fnrsCnsltnMng;
const payload = {
return {
fnrsCnsltnMng: {
...fnrsCnsltnMngRest,
taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
@@ -106,11 +109,71 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
selected: item.selected ?? undefined,
})),
esntlAtchFileList: data.esntlAtchFileList,
splmntDmndList: data.splmntDmndList.map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
...item,
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
}),
),
};
}
async function submitForm(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: "SV" | "ST",
): Promise<string | undefined> {
const payload = buildFormPayload(data, prgrsSttsComCd);
if (!payload) return undefined;
return fnrsCnsltnMngService.saveApply(payload);
}
async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise<string | undefined> {
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
if (!docNo) return undefined;
const payload = buildFormPayload(data, "TE");
if (!payload) return undefined;
return fnrsCnsltnMngService.submitSupplement(docNo, payload);
}
async function onSplmntCmptn(data: FnrsCnsltnRegistFormData) {
if (isSubmitting) return;
const hasEmptySplmntCn = data.splmntDmndList?.some(
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
return;
}
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
if (!docNo) {
alert("문서번호가 없습니다.");
return;
}
try {
setIsSubmitting(true);
const result = await submitSupplementForm(data);
if (!result) return;
alert("보완완료 처리되었습니다.");
router.push(`/fnrs-cnsltn/${docNo}`);
queryClient.invalidateQueries({ queryKey: fnrsCnsltnMngBaseKey });
queryClient.invalidateQueries({
queryKey: [...fnrsCnsltnMngBaseKey, "findById", docNo],
});
} catch (error) {
console.error("보완완료 실패:", error);
alert("보완완료 처리 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
}
async function onSave(data: FnrsCnsltnRegistFormData) {
if (isSubmitting) return;
@@ -210,6 +273,7 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
/>
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
@@ -221,23 +285,40 @@ function FnrsFormPage({ id, mode, initialData, taskSeComCd }: FnrsFormPageProps)
>
목록
</Button>
<Button type="submit" disabled={isSubmitting}>
저장
</Button>
{isEditMode && (
{!isSplmntPrgrsStts ? (
<>
<Button
color="green"
type="button"
onClick={handleApplyClick}
disabled={isSubmitting}
>
신청완료
</Button>
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
삭제
<Button type="submit" disabled={isSubmitting}>
저장
</Button>
{isEditMode && (
<>
<Button
color="green"
type="button"
onClick={handleApplyClick}
disabled={isSubmitting}
>
신청완료
</Button>
<Button
color="red"
type="button"
onClick={handleDelClick}
disabled={isSubmitting}
>
삭제
</Button>
</>
)}
</>
) : (
<Button
type="button"
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완완료
</Button>
)}
</div>
</div>
@@ -5,7 +5,7 @@ import { useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { getEditableReviewStep } from "../lib/review-step-utils";
@@ -19,7 +19,6 @@ import { BusinessScaleSection } from "./sections/business-scale-section";
import { ChecklistSection } from "./sections/checklist-section";
import { ContactInfoSection } from "./sections/contact-info-section";
import { FnrsSharSection } from "./sections/fnrs-shar-section";
import { SupplementDemandSection } from "./sections/supplement-demand-section";
import {
TechReviewOpinionSection,
type TechReviewOpinionSectionHandle,
@@ -1,57 +0,0 @@
"use client";
import { dayjs } from "@/shared/lib";
import type { FnrsCnsltnSplmntDmndResponseDto } from "@/entities/fnrs-cnsltn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Badge,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
interface SupplementDemandSectionProps {
splmntDmndList: FnrsCnsltnSplmntDmndResponseDto[];
}
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
if (splmntDmndList.length === 0) return null;
return (
<SectionCollapsible title="보완요청">
<Table variant="secondary">
<TableHeader>
<TableRow>
<TableHead className="w-[8rem]">항목</TableHead>
<TableHead>보완요청 내용</TableHead>
<TableHead>보완 내용</TableHead>
<TableHead className="w-[8rem]">완료여부</TableHead>
<TableHead className="w-[12rem]">요청일시</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{splmntDmndList.map((item) => (
<TableRow key={item.splmntDmndSn}>
<TableCell>{item.splmntArtclCdNm || "-"}</TableCell>
<TableCell className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</TableCell>
<TableCell className="whitespace-pre-wrap">{item.splmntCn || "-"}</TableCell>
<TableCell>
<Badge color={item.cmptnYn === "Y" ? "green" : "gray"}>
{item.cmptnYn === "Y" ? "완료" : "미완료"}
</Badge>
</TableCell>
<TableCell>
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</SectionCollapsible>
);
}
export { SupplementDemandSection };
@@ -2,7 +2,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = {
strg: "strg",
aply: "aply",
del: "del",
splmntCmptn: "splmntCmptn",
instlSncnAncmnt: "instlSncnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
@@ -32,7 +31,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, InstlSncnViewBut
INSTL_SNCN_VIEW_BUTTON_ACTION.aply,
INSTL_SNCN_VIEW_BUTTON_ACTION.del,
],
SL: [INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn],
TA: [INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt],
},
ROLE_USER04: {
@@ -44,7 +44,6 @@ import {
toSplmntDmndSaveItem,
} from "../../lib/save-payload-utils";
import { type ChecklistFormState } from "../../model/view-types";
import type { SupplementDemandSectionHandle } from "../sections/supplement-demand-section";
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
interface FloatingActionButtonsProps {
@@ -55,7 +54,6 @@ interface FloatingActionButtonsProps {
dtlInstCd: string | undefined;
checklistFormState: ChecklistFormState;
techReviewOpinionRef?: React.RefObject<TechReviewOpinionSectionHandle | null>;
supplementDemandRef?: React.RefObject<SupplementDemandSectionHandle | null>;
opnrNm?: string;
}
@@ -69,7 +67,6 @@ function FloatingActionButtons({
dtlInstCd,
checklistFormState,
techReviewOpinionRef,
supplementDemandRef,
opnrNm,
}: FloatingActionButtonsProps) {
const router = useRouter();
@@ -397,9 +394,7 @@ function FloatingActionButtons({
getReviewOpinionFormData(),
opnrNm,
),
splmntDmndList:
supplementDemandRef?.current?.getSplmntDmndList() ??
(data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
};
await executeAction({
@@ -451,16 +446,6 @@ function FloatingActionButtons({
삭제
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn) && (
<Button
onClick={() =>
void executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.splmntCmptn })
}
disabled={isProcessing}
>
보완완료
</Button>
)}
{shouldShowButton(INSTL_SNCN_VIEW_BUTTON_ACTION.instlSncnAncmnt) && (
<Button onClick={handleInstlSncnAncmntClick} disabled={isProcessing}>
설치인가 고시
@@ -575,19 +560,6 @@ async function handleDel(context: ActionHandlerContext): Promise<void> {
}
}
async function handleSplmntCmptn(
context: ActionHandlerContext<InstlSncnApplySaveRequestDto>,
): Promise<void> {
try {
const docNo = context.data.detDto?.instlSncnDocNo;
if (!docNo) throw new Error("문서번호가 없습니다.");
await instlSncnService.submitSupplement(docNo, context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleChangePrgrsSttsComCd(
context: ActionHandlerContext<DocPrgrsUpdateRequestDto>,
): Promise<void> {
@@ -648,7 +620,6 @@ const buttonActionHandlers: Record<InstlSncnViewButtonAction, ActionHandler> = {
strg: handleSaveApply,
aply: handleSaveApply,
del: handleDel,
splmntCmptn: handleSplmntCmptn,
instlSncnAncmnt: handleChangePrgrsSttsComCd,
envoSbmsn: handleChangePrgrsSttsComCd,
rjct: handleChangePrgrsSttsComCd,
@@ -83,7 +83,10 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
esntlAtchFileList: data.esntlAtchFileList,
splmntDmndList: data.splmntDmndList.map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => item,
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
...item,
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
}),
),
};
}
@@ -104,6 +107,14 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp
async function onSplmntCmptn(data: InstlSncnRegistFormData) {
if (isSubmitting) return;
const hasEmptySplmntCn = data.splmntDmndList?.some(
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요.");
return;
}
const docNo = initialData?.detDto.instlSncnDocNo;
if (!docNo) {
alert("문서번호가 없습니다.");
@@ -5,7 +5,7 @@ import { useRef } from "react";
import { useUserStore } from "@/shared/lib";
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
import { useInstlSncnDetail } from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { SectionCollapsible, SupplementDemandSection } from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { getEditableReviewStep } from "../lib/review-step-utils";
@@ -18,10 +18,6 @@ import { BusinessCostSection } from "./sections/business-cost-section";
import { BusinessScaleSection } from "./sections/business-scale-section";
import { ChecklistSection } from "./sections/checklist-section";
import { ContactInfoSection } from "./sections/contact-info-section";
import {
SupplementDemandSection,
type SupplementDemandSectionHandle,
} from "./sections/supplement-demand-section";
import {
TechReviewOpinionSection,
type TechReviewOpinionSectionHandle,
@@ -34,7 +30,6 @@ interface InstlViewPageProps {
function InstlViewPage({ id }: InstlViewPageProps) {
const userInfo = useUserStore((state) => state.userInfo);
const techReviewOpinionRef = useRef<TechReviewOpinionSectionHandle>(null);
const supplementDemandRef = useRef<SupplementDemandSectionHandle>(null);
const { data, isLoading, isError } = useInstlSncnDetail(id);
const { formState: checklistFormState, setFormState: setChecklistFormState } =
useChecklistFormState(data?.cklstResultList ?? EMPTY_CKLST_RESULT_LIST);
@@ -100,11 +95,7 @@ function InstlViewPage({ id }: InstlViewPageProps) {
rvwList={rvwList}
editableReviewStep={editableReviewStep}
/>
<SupplementDemandSection
ref={supplementDemandRef}
splmntDmndList={splmntDmndList}
canEditCmptnYn={userInfo?.dtlInstCd === "ROLE_USER01"}
/>
<SupplementDemandSection splmntDmndList={splmntDmndList} />
<FloatingActionButtons
data={data}
docSeComCd="INSTL"
@@ -113,7 +104,6 @@ function InstlViewPage({ id }: InstlViewPageProps) {
dtlInstCd={userInfo?.dtlInstCd}
checklistFormState={checklistFormState}
techReviewOpinionRef={techReviewOpinionRef}
supplementDemandRef={supplementDemandRef}
opnrNm={userInfo?.userName}
/>
</div>
+1
View File
@@ -36,4 +36,5 @@ export {
type SplmntDmndDialogConfirmPayload,
} 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";
@@ -1,181 +1,114 @@
"use client";
import { forwardRef, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { dayjs } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
InstlSncnSplmntDmndResponseDto,
InstlSncnSplmntDmndSaveDto,
} from "@/entities/instl-sncn";
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import {
Badge,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { toSplmntDmndSaveItem } from "../../lib/save-payload-utils";
interface SupplementDemandSectionProps {
splmntDmndList: InstlSncnSplmntDmndResponseDto[];
canEditCmptnYn: boolean;
}
export interface SupplementDemandSectionHandle {
getSplmntDmndList: () => InstlSncnSplmntDmndSaveDto[];
}
const SupplementDemandSection = forwardRef<
SupplementDemandSectionHandle,
SupplementDemandSectionProps
>(function SupplementDemandSection({ splmntDmndList, canEditCmptnYn }, ref) {
const [cmptnYnBySn, setCmptnYnBySn] = useState<Record<number, string>>({});
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
useImperativeHandle(
ref,
() => ({
getSplmntDmndList: () =>
splmntDmndList.map((item) => {
const saveItem = toSplmntDmndSaveItem(item);
const sn = item.splmntDmndSn;
if (sn == null || cmptnYnBySn[sn] == null) return saveItem;
return { ...saveItem, cmptnYn: cmptnYnBySn[sn] };
}),
}),
[splmntDmndList, cmptnYnBySn],
);
function getCmptnYn(item: InstlSncnSplmntDmndResponseDto) {
const sn = item.splmntDmndSn;
if (sn != null && cmptnYnBySn[sn] != null) return cmptnYnBySn[sn];
return item.cmptnYn ?? "N";
}
return (
<SectionCollapsible title="보완요청 및 완료 현황">
<div className="overflow-x-auto">
<Table variant="secondary">
<colgroup>
<col width="14%" />
<col width="8%" />
<col width="30%" />
<col width="8%" />
<col width="30%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>보완항목</TableHead>
<TableHead>보완요청일시</TableHead>
<TableHead>보완요청내용</TableHead>
<TableHead>보완완료일시</TableHead>
<TableHead>보완완료내용</TableHead>
<TableHead>완료여부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{splmntDmndList.map((item) => {
const sn = item.splmntDmndSn;
const cmptnYn = getCmptnYn(item);
const dmndAtchFileNm = item.splmntDmndAtchFileInfo?.orgnlFileNm;
const cmptnAtchFileNm = item.splmntAtchFileInfo?.orgnlFileNm;
return (
<TableRow key={sn ?? `${item.splmntArtclComCd}-${item.splmntDmndDt}`}>
<TableCell align="center" className="align-top">
{item.splmntArtclCdNm || "-"}
</TableCell>
<TableCell align="center" className="whitespace-nowrap align-top">
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
</TableCell>
<TableCell className="align-top">
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
{item.splmntDmndAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.splmntDmndAtchFileGroupId!)}
>
<Paperclip className="my-1 size-5 shrink-0" />
{dmndAtchFileNm || "다운로드"}
</a>
) : null}
</div>
</TableCell>
<TableCell align="center" className="whitespace-nowrap align-top">
{item.splmntCmptnDt
? dayjs(item.splmntCmptnDt).format("YYYY-MM-DD HH:mm")
: "-"}
</TableCell>
<TableCell className="align-top">
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
{item.splmntAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.splmntAtchFileGroupId!)}
>
<Paperclip className="my-1 size-5 shrink-0" />
{cmptnAtchFileNm || "다운로드"}
</a>
) : null}
</div>
</TableCell>
<TableCell align="center" className="align-top">
{canEditCmptnYn && sn != null ? (
<FieldSet>
<RadioGroup
value={cmptnYn}
onValueChange={(value) =>
setCmptnYnBySn((prev) => ({ ...prev, [sn]: value }))
}
className="flex justify-center gap-4"
>
<FieldLabel
htmlFor={`splmnt-cmptn-y-${sn}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="Y" id={`splmnt-cmptn-y-${sn}`} />
<span className="font-normal">완료</span>
</Field>
</FieldLabel>
<FieldLabel
htmlFor={`splmnt-cmptn-n-${sn}`}
className="!w-auto border-none"
>
<Field orientation="horizontal" className="w-auto">
<RadioGroupItem value="N" id={`splmnt-cmptn-n-${sn}`} />
<span className="font-normal">미완료</span>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
) : (
<Badge size="lg" color={cmptnYn === "Y" ? "green" : "gray"}>
{cmptnYn === "Y" ? "완료" : "미완료"}
</Badge>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</SectionCollapsible>
);
});
export { SupplementDemandSection };
"use client";
import { Paperclip } from "lucide-react";
import { dayjs } from "@/shared/lib";
import type { SplmntDmndResponseExtension, SplmntDmndSaveBase } from "@/shared/model";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import {
Badge,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { SectionCollapsible } from "./section-collapsible";
type SplmntDmndItem = SplmntDmndSaveBase & SplmntDmndResponseExtension;
interface SupplementDemandSectionProps {
splmntDmndList: SplmntDmndItem[];
}
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
return (
<SectionCollapsible title="보완요청 및 완료 현황">
<div className="overflow-x-auto">
<Table variant="secondary">
<colgroup>
<col width="14%" />
<col width="8%" />
<col width="30%" />
<col width="8%" />
<col width="30%" />
<col width="10%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead>보완항목</TableHead>
<TableHead>보완요청일시</TableHead>
<TableHead>보완요청내용</TableHead>
<TableHead>보완완료일시</TableHead>
<TableHead>보완완료내용</TableHead>
<TableHead>완료여부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{splmntDmndList.map((item) => {
const sn = item.splmntDmndSn;
const cmptnYn = item.cmptnYn ?? "N";
const dmndAtchFileNm = item.splmntDmndAtchFileInfo?.orgnlFileNm;
const cmptnAtchFileNm = item.splmntAtchFileInfo?.orgnlFileNm;
return (
<TableRow key={sn ?? `${item.splmntArtclComCd}-${item.splmntDmndDt}`}>
<TableCell align="center" className="align-top">
{item.splmntArtclCdNm || "-"}
</TableCell>
<TableCell align="center" className="whitespace-nowrap align-top">
{item.splmntDmndDt ? dayjs(item.splmntDmndDt).format("YYYY-MM-DD HH:mm") : "-"}
</TableCell>
<TableCell className="align-top">
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
{item.splmntDmndAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.splmntDmndAtchFileGroupId!)}
>
<Paperclip className="my-1 size-5 shrink-0" />
{dmndAtchFileNm || "다운로드"}
</a>
) : null}
</div>
</TableCell>
<TableCell align="center" className="whitespace-nowrap align-top">
{item.splmntCmptnDt
? dayjs(item.splmntCmptnDt).format("YYYY-MM-DD HH:mm")
: "-"}
</TableCell>
<TableCell className="align-top">
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
{item.splmntAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.splmntAtchFileGroupId!)}
>
<Paperclip className="my-1 size-5 shrink-0" />
{cmptnAtchFileNm || "다운로드"}
</a>
) : null}
</div>
</TableCell>
<TableCell align="center" className="align-top">
<Badge size="lg" color={cmptnYn === "Y" ? "green" : "gray"}>
{cmptnYn === "Y" ? "완료" : "미완료"}
</Badge>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</SectionCollapsible>
);
}
export { SupplementDemandSection };