[as] feat: 재원협의 기술검토 로직 작업

This commit is contained in:
2026-09-11 17:02:09 +09:00
parent 01d5ed297e
commit d1ccddc1d3
16 changed files with 415 additions and 21 deletions
@@ -8,11 +8,17 @@ import type {
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_MNG;
async function findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto> {
const response = await client.get<PwrkCnsltnMngDetResponseDto>(`${BASE}/${pwrkCnsltnDocNo}`);
return response.data;
}
async function findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto> {
const response = await client.get<PwrkCnsltnMngDetResponseDto>(`${BASE}/updocno/${upDocNo}`);
return response.data;
}
export const pwrkCnsltnMngService = {
findById,
findByUpDocNo,
} satisfies PwrkCnsltnMngService;
@@ -4,9 +4,21 @@ import { pwrkCnsltnMngService } from "./pwrk-cnsltn-mng-service";
export const pwrkCnsltnMngKeys = {
all: ["pwrk-cnsltn-mng"] as const,
byId: (pwrkCnsltnDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-id", pwrkCnsltnDocNo] as const,
byUpDocNo: (upDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-up-doc-no", upDocNo] as const,
};
export function usePwrkCnsltnMng(
pwrkCnsltnDocNo: string | null | undefined,
isEnabled = true,
) {
return useQuery({
queryKey: pwrkCnsltnMngKeys.byId(pwrkCnsltnDocNo!),
queryFn: () => pwrkCnsltnMngService.findById(pwrkCnsltnDocNo!),
enabled: Boolean(pwrkCnsltnDocNo) && isEnabled,
});
}
export function usePwrkCnsltnMngByUpDocNo(upDocNo: string | null | undefined, isEnabled = true) {
return useQuery({
queryKey: pwrkCnsltnMngKeys.byUpDocNo(upDocNo!),
@@ -1,9 +1,15 @@
export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service";
export { pwrkCnsltnMngKeys, usePwrkCnsltnMngByUpDocNo } from "./api/queries";
export {
pwrkCnsltnMngKeys,
usePwrkCnsltnMng,
usePwrkCnsltnMngByUpDocNo,
} from "./api/queries";
export { findCompletedPwrkDraft } from "./lib/find-completed-draft";
export type {
PwrkCnsltnCnsltnResponseDto,
PwrkCnsltnDocCrtDmndResponseDto,
PwrkCnsltnDocCrtRsltResponseDto,
PwrkCnsltnMngDetResponseDto,
PwrkCnsltnMngService,
PwrkCnsltnPjtcoAjmtResponseDto,
} from "./model/pwrk-cnsltn-mng-interface";
@@ -1,3 +1,5 @@
import type { AtflMngClientResponseDto } from "@/shared/model";
export interface PwrkCnsltnDocCrtRsltResponseDto {
crtDmndUuid: string;
verSn: number;
@@ -10,25 +12,80 @@ export interface PwrkCnsltnDocCrtRsltResponseDto {
export interface PwrkCnsltnDocCrtDmndResponseDto {
crtDmndUuid: string;
pwrkCnsltnDocNo: string;
orgnlDocNo?: string;
orgnlDocSeComCd?: string;
orgnlDocSeComCdNm?: string;
crtSysDmndId?: string;
crtSttsComCd?: string;
crtSttsComCdNm?: string;
failRsn?: string;
dmndDt?: string;
cmptnDt?: string;
rqstrUuid?: string;
rqstrNm?: string;
crtRslt?: PwrkCnsltnDocCrtRsltResponseDto | null;
}
/** 백엔드 PwrkCnsltnMngDetResponseDto.CnsltnResponseDto */
export interface PwrkCnsltnCnsltnResponseDto {
pwrkCnsltnDocNo?: string;
rvwOpnnSn?: number;
rvwOpnnClsfComCd?: string;
rvwOpnnClsfComCdNm?: string;
rvwOpnn?: string;
cnsltnCn?: string;
rfltYn?: string;
rmrk?: string;
atchFileGroupId?: string;
atchFileInfo?: AtflMngClientResponseDto;
rvwrNm?: string;
rvwrMblTelno?: string;
rvwrOgdpNm?: string;
rvwrEmlAddr?: string;
lclgvPicNm?: string;
lclgvPicTelno?: string;
lclgvPicOgdpNm?: string;
lclgvPicEmlAddr?: string;
crtDmndUuid?: string;
crtYn?: string;
hwrtChgYn?: string;
verSn?: number;
dtlSn?: number;
}
/** 백엔드 PwrkCnsltnMngDetResponseDto.PjtcoAjmtResponseDto */
export interface PwrkCnsltnPjtcoAjmtResponseDto {
pwrkCnsltnDocNo?: string;
rvwOpnnSn?: number;
cnstpSeCd?: string;
rvwOpnnClsfComCd?: string;
rvwOpnnClsfComCdNm?: string;
ognTpcoAmt?: number;
ognGvexpAmt?: number;
ognLcexpAmt?: number;
chgTpcoAmt?: number;
chgGvexpAmt?: number;
chgLcexpAmt?: number;
}
export interface PwrkCnsltnMngDetResponseDto {
pwrkCnsltnDocNo: string;
upDocNo: string;
upDocSeComCd?: string;
upDocSeComCdNm?: string;
prgrsSttsComCd?: string;
prgrsSttsComCdNm?: string;
bizNm?: string;
atchFileGroupId?: string;
regDt?: string;
rgtrNm?: string;
cnsltnList?: PwrkCnsltnCnsltnResponseDto[];
pjtcoAjmtList?: PwrkCnsltnPjtcoAjmtResponseDto[];
crtDmndList?: PwrkCnsltnDocCrtDmndResponseDto[];
crtRsltList?: PwrkCnsltnDocCrtRsltResponseDto[];
}
export interface PwrkCnsltnMngService {
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
}
@@ -169,7 +169,9 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
<SectionCollapsibleGroup>
<SectionCollapsibleToggle />
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
{initialData?.detDto?.designCnstnDocNo && (
<ProgressStatusSection
@@ -12,6 +12,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
dsgntRvwr: "dsgntRvwr",
pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt",
pwrkCnsltnDocMng: "pwrkCnsltnDocMng",
rvwRsltInq: "rvwRsltInq",
rvwStrg: "rvwStrg",
rvwCmptn: "rvwCmptn",
dmndSplmnt: "dmndSplmnt",
@@ -59,11 +60,15 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
ATRZ02: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
],
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv],
ATRZ03: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
],
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply],
},
RK: {
@@ -81,7 +86,7 @@ interface GetButtonConfigsOptions {
}
const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
]);
@@ -109,8 +114,14 @@ function resolveRoleUser01AtrzButtonActions(
}
return actions.filter((action) => {
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false;
if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) {
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
if (
action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng ||
action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq
) {
return options.hasCompletedDraft;
}
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
@@ -118,9 +129,7 @@ function resolveRoleUser01AtrzButtonActions(
}
}
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
return !needsPendingApproval || options?.hasPendingApprovalRequest !== false;
return true;
});
}
@@ -86,7 +86,7 @@ export const fnrsCnsltnRegistSchema = z.object({
splmntDmndList: z.array(
z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().optional(),
splmntArtclComCd: z.string().nullable(),
splmntArtclCdNm: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
@@ -10,6 +10,7 @@ import type {
FnrsCnsltnMngResponseDto,
FnrsCnsltnSplmntDmndItem,
} from "@/entities/fnrs-cnsltn";
import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng";
import {
mapFnrsSharDetToFormList,
type SplmntDmndDialogConfirmPayload,
@@ -129,6 +130,26 @@ export function buildFileSplmntReqPayload(
};
}
export function buildRvwRsltSplmntReqPayload(
data: FnrsCnsltnMngDetResponseDto,
selected: PwrkCnsltnCnsltnResponseDto[],
checklistFormState: ChecklistFormState,
): FnrsCnsltnApplySaveRequestDto {
const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) =>
toSplmntDmndSaveItem(item),
);
const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({
splmntDmndCn: row.rvwOpnn,
cmptnYn: "N",
}));
return {
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
};
}
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
return {
splmntDmndSn: item.splmntDmndSn,
@@ -28,7 +28,11 @@ import {
fnrsCnsltnMngService,
type FnrsExamineRequestDto,
} from "@/entities/fnrs-cnsltn";
import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng";
import {
findCompletedPwrkDraft,
type PwrkCnsltnCnsltnResponseDto,
usePwrkCnsltnMngByUpDocNo,
} from "@/entities/pwrk-cnsltn-mng";
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
@@ -41,6 +45,7 @@ import {
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
import {
buildFileSplmntReqPayload,
buildRvwRsltSplmntReqPayload,
buildSavePayload,
buildSupplementReqPayload,
buildTechReviewReplyPayload,
@@ -51,6 +56,7 @@ import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opi
import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog";
import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog";
import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog";
interface FloatingActionButtonsProps {
data: FnrsCnsltnMngDetResponseDto;
@@ -73,6 +79,7 @@ type ActiveDialog =
| "fileSplmnt"
| "elaprApproval"
| "pwrkCnsltnDocMng"
| "rvwRsltInq"
| null;
function FloatingActionButtons({
@@ -311,7 +318,7 @@ function FloatingActionButtons({
data,
checklistFormState,
"ENV",
"EP",
"RM",
"CP",
getReviewOpinionFormData(),
opnrNm,
@@ -320,7 +327,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
request: payload,
successMessage: "검토결과 회신이 완료되었습니다.",
successMessage: "재원협의 결과 회신이 완료되었습니다.",
});
techReviewOpinionRef?.current?.resetForm();
}
@@ -435,6 +442,16 @@ function FloatingActionButtons({
});
}
async function handleRvwRsltSplmntConfirm(selected: PwrkCnsltnCnsltnResponseDto[]) {
setActiveDialog(null);
const requestPayload = buildRvwRsltSplmntReqPayload(data, selected, checklistFormState);
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
request: requestPayload,
successMessage: "보완요청이 처리되었습니다.",
});
}
return (
<>
<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)]">
@@ -486,7 +503,7 @@ function FloatingActionButtons({
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
검토결과 회신
재원협의 결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && (
@@ -518,6 +535,15 @@ function FloatingActionButtons({
실무협의서 관리
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && (
<Button
color="sky"
onClick={() => setActiveDialog("rvwRsltInq")}
disabled={isProcessing}
>
검토결과조회
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && (
<Button onClick={handleRvwStrgClick} disabled={isProcessing}>
저장
@@ -588,8 +614,17 @@ function FloatingActionButtons({
<PwrkCnsltnDocMngDialog
open={activeDialog === "pwrkCnsltnDocMng"}
fnrsCnsltnDocNo={docNo}
atrzSeComCdList={atrzSeComCdList}
onClose={() => setActiveDialog(null)}
/>
<PwrkCnsltnRvwRsltInqDialog
open={activeDialog === "rvwRsltInq"}
fnrsCnsltnDocNo={docNo}
isSubmitting={isProcessing}
onClose={() => setActiveDialog(null)}
onConfirm={handleRvwRsltSplmntConfirm}
/>
</>
)}
</>
@@ -704,6 +739,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
dsgntRvwr: handleDsgntRvwr,
pwrkCnsltnDocCrt: () => Promise.resolve(),
pwrkCnsltnDocMng: () => Promise.resolve(),
rvwRsltInq: () => Promise.resolve(),
rvwStrg: handleTechReview,
rvwCmptn: handleTechReview,
dmndSplmnt: handleDmndSplmnt,
@@ -42,9 +42,12 @@ const HangulGiangi = dynamic(
const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
interface PwrkCnsltnDocMngDialogProps {
open: boolean;
fnrsCnsltnDocNo: string;
atrzSeComCdList: string[];
onClose: () => void;
}
@@ -59,7 +62,12 @@ function createPwrkAtchUploadKey(file: File) {
return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDocMngDialogProps) {
function PwrkCnsltnDocMngDialog({
open,
fnrsCnsltnDocNo,
atrzSeComCdList,
onClose,
}: PwrkCnsltnDocMngDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const downloadByAtflId = useDownloadAttachmentByAtflId();
@@ -235,6 +243,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo
}
const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]);
const canSave = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code));
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
@@ -263,6 +272,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo
documentBlob={documentBlob}
documentKey={completedDraft.crtDmndUuid}
documentFileName={documentFileName}
canSave={canSave}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
/>
@@ -0,0 +1,224 @@
"use client";
import { useState } from "react";
import {
type PwrkCnsltnCnsltnResponseDto,
usePwrkCnsltnMngByUpDocNo,
} from "@/entities/pwrk-cnsltn-mng";
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Skeleton,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
interface PwrkCnsltnRvwRsltInqDialogProps {
open: boolean;
fnrsCnsltnDocNo: string;
isSubmitting?: boolean;
onClose: () => void;
onConfirm: (selected: PwrkCnsltnCnsltnResponseDto[]) => void;
}
interface CnsltnListTableProps {
rows: PwrkCnsltnCnsltnResponseDto[];
selectedRowKeys: Set<string>;
onToggleAll: (checked: boolean) => void;
onToggleRow: (rowKey: string, checked: boolean) => void;
}
function PwrkCnsltnRvwRsltInqDialog({
open,
fnrsCnsltnDocNo,
isSubmitting = false,
onClose,
onConfirm,
}: PwrkCnsltnRvwRsltInqDialogProps) {
const [selectedRowKeys, setSelectedRowKeys] = useState<Set<string>>(new Set());
const {
data: pwrkCnsltnData,
isLoading,
isError,
} = usePwrkCnsltnMngByUpDocNo(fnrsCnsltnDocNo, open);
const cnsltnList = pwrkCnsltnData?.cnsltnList ?? [];
const selectedRows = cnsltnList.filter((row, index) =>
selectedRowKeys.has(getCnsltnRowKey(row, index)),
);
function handleClose() {
setSelectedRowKeys(new Set());
onClose();
}
function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) handleClose();
}
function handleToggleAll(checked: boolean) {
if (!checked) {
setSelectedRowKeys(new Set());
return;
}
setSelectedRowKeys(new Set(cnsltnList.map((row, index) => getCnsltnRowKey(row, index))));
}
function handleToggleRow(rowKey: string, checked: boolean) {
setSelectedRowKeys((prev) => {
const next = new Set(prev);
if (checked) next.add(rowKey);
else next.delete(rowKey);
return next;
});
}
function handleConfirm() {
if (selectedRows.length === 0) return;
onConfirm(selectedRows);
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="max-h-[96vh] max-w-[120rem] overflow-y-auto"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>검토결과조회</DialogTitle>
</DialogHeader>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-16 w-full" />
<Skeleton className="h-64 w-full" />
</div>
) : null}
{isError ? (
<p className="py-16 text-center text-lg text-destructive">
실무협의 검토결과를 불러오지 못했습니다.
</p>
) : null}
{!isLoading && !isError && pwrkCnsltnData ? (
<div className="h-[64rem] overflow-x-auto">
{cnsltnList.length === 0 ? (
<p className="py-10 text-center text-muted-foreground">조회된 검토의견이 없습니다.</p>
) : (
<CnsltnListTable
rows={cnsltnList}
selectedRowKeys={selectedRowKeys}
onToggleAll={handleToggleAll}
onToggleRow={handleToggleRow}
/>
)}
</div>
) : null}
<DialogFooter>
<Button type="button" color="white" onClick={handleClose} disabled={isSubmitting}>
닫기
</Button>
<Button
type="button"
onClick={handleConfirm}
disabled={isSubmitting || selectedRows.length === 0}
>
보완요청
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function CnsltnListTable({
rows,
selectedRowKeys,
onToggleAll,
onToggleRow,
}: CnsltnListTableProps) {
const selectedCount = rows.filter((row, index) =>
selectedRowKeys.has(getCnsltnRowKey(row, index)),
).length;
const isAllSelected = rows.length > 0 && selectedCount === rows.length;
const isSomeSelected = selectedCount > 0 && !isAllSelected;
return (
<Table variant="secondary">
<colgroup>
<col width="8%" />
<col width="42%" />
<col width="50%" />
</colgroup>
<TableHeader className="sticky top-0">
<TableRow>
<TableHead className="w-14" align="center">
<Checkbox
checked={isAllSelected ? true : isSomeSelected ? "indeterminate" : false}
onCheckedChange={(checked) => onToggleAll(checked === true)}
aria-label="전체 선택"
/>
</TableHead>
<TableHead>분류</TableHead>
<TableHead>검토의견</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row, index) => {
const rowKey = getCnsltnRowKey(row, index);
const isSelected = selectedRowKeys.has(rowKey);
return (
<TableRow
key={rowKey}
data-state={isSelected ? "selected" : undefined}
className={isSelected ? "!bg-primary-transparent" : undefined}
>
<TableCell align="center">
<Checkbox
checked={isSelected}
onCheckedChange={(checked) => onToggleRow(rowKey, checked === true)}
aria-label="행 선택"
/>
</TableCell>
<TableCell>
<p className="whitespace-pre-wrap">{formatOptional(row.rmrk)}</p>
</TableCell>
<TableCell>
<p className="whitespace-pre-wrap">{formatOptional(row.rvwOpnn)}</p>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}
function getCnsltnRowKey(row: PwrkCnsltnCnsltnResponseDto, index: number) {
return [row.pwrkCnsltnDocNo, row.verSn, row.dtlSn, row.rvwOpnnSn, String(index)]
.filter((value) => value !== undefined && value !== null && value !== "")
.join("::");
}
function formatOptional(value: string | number | undefined | null) {
if (value === undefined || value === null) return "-";
const text = String(value).trim();
return text ? text : "-";
}
export { PwrkCnsltnRvwRsltInqDialog };
export type { PwrkCnsltnRvwRsltInqDialogProps };
@@ -225,7 +225,9 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
<SectionCollapsibleGroup>
<SectionCollapsibleToggle />
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
{initialData?.detDto.instlSncnDocNo && (
<ProgressStatusSection
@@ -242,7 +242,9 @@ function SewerBscPlanFormPage({
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="mt-6 space-y-4">
<SectionCollapsibleGroup>
<SectionCollapsibleToggle />
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
<SectionCollapsibleToggle />
</div>
<div className="space-y-4">
{initialData?.detDto?.sewerBscPlanDocNo && (
<ProgressStatusSection
+1 -1
View File
@@ -203,7 +203,7 @@ export interface EsntlAtchFileSaveBase {
/** 보완요구 저장 공통 필드 */
export interface SplmntDmndSaveBase {
splmntDmndSn?: number | null;
splmntArtclComCd?: string;
splmntArtclComCd?: string | null;
splmntDmndCn?: string;
splmntDmndDt?: string;
splmntDmndAtchFileGroupId?: string;
@@ -6,6 +6,7 @@ import { Button } from "@common/components";
interface HangulGiangiToolbarProps {
isBusy?: boolean;
canSave?: boolean;
onSave: () => void;
onDownload: () => void;
onFullscreen: () => void;
@@ -13,16 +14,19 @@ interface HangulGiangiToolbarProps {
function HangulGiangiToolbar({
isBusy = false,
canSave = true,
onSave,
onDownload,
onFullscreen,
}: HangulGiangiToolbarProps) {
return (
<div className="flex w-full flex-nowrap items-center gap-2 border-b border-border bg-background p-3">
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onSave}>
<Save className="!size-5" />
저장
</Button>
{canSave && (
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onSave}>
<Save className="!size-5" />
저장
</Button>
)}
<Button type="button" color="white" size="sm" disabled={isBusy} onClick={onDownload}>
<Download className="!size-5" />
다운로드
@@ -21,6 +21,7 @@ function HangulGiangi({
documentFileName,
onSaveDocument,
onDownload,
canSave = true,
bridgeUrl = DEFAULT_BRIDGE_URL,
ctrlBaseUrl = WEBHYP_CTRL_URL,
className,
@@ -183,6 +184,7 @@ function HangulGiangi({
>
<HangulGiangiToolbar
isBusy={isBusy || !isCtrlReady}
canSave={canSave}
onSave={handleSave}
onDownload={handleDownload}
onFullscreen={handleFullscreen}
@@ -296,6 +298,7 @@ interface HangulGiangiProps {
documentFileName: string;
onSaveDocument: (file: Blob) => Promise<void>;
onDownload: () => Promise<void>;
canSave?: boolean;
bridgeUrl?: string;
ctrlBaseUrl?: string;
className?: string;