Merge branch 'iipbs/as/dev'
This commit is contained in:
@@ -16,7 +16,14 @@ async function downloadAttachmentByAtflCd(atflCd: string): Promise<AxiosResponse
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAttachmentByAtflId(atflId: string): Promise<AxiosResponse> {
|
||||
return await client.get<Blob>(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID}/${atflId}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
|
||||
export const attachService = {
|
||||
deleteAttachmentByAtflId,
|
||||
downloadAttachmentByAtflCd,
|
||||
downloadAttachmentByAtflId,
|
||||
} satisfies AttachService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
@@ -8,6 +9,22 @@ import { attachService } from "./attach-service";
|
||||
|
||||
const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const;
|
||||
const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const;
|
||||
const attachDownloadByAtflIdKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD_BY_ATFL_ID] as const;
|
||||
|
||||
function triggerBlobDownload(response: AxiosResponse) {
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
export function useDeleteAttachment() {
|
||||
return useMutation({
|
||||
@@ -21,19 +38,17 @@ export function useDownloadAttachmentByAtflCd() {
|
||||
mutationKey: [...attachDownloadBaseKey, "download"],
|
||||
mutationFn: async (atflCd: string) => {
|
||||
const response = await attachService.downloadAttachmentByAtflCd(atflCd);
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
triggerBlobDownload(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDownloadAttachmentByAtflId() {
|
||||
return useMutation({
|
||||
mutationKey: [...attachDownloadByAtflIdKey, "download"],
|
||||
mutationFn: async (atflId: string) => {
|
||||
const response = await attachService.downloadAttachmentByAtflId(atflId);
|
||||
triggerBlobDownload(response);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export { attachService } from "./api/attach-service";
|
||||
export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries";
|
||||
export {
|
||||
useDeleteAttachment,
|
||||
useDownloadAttachmentByAtflCd,
|
||||
useDownloadAttachmentByAtflId,
|
||||
} from "./api/queries";
|
||||
export type { Attach, AttachService } from "./model/attach-interface";
|
||||
|
||||
@@ -19,4 +19,5 @@ export interface Attach {
|
||||
export interface AttachService {
|
||||
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
|
||||
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
|
||||
downloadAttachmentByAtflId: (atflId: string) => Promise<AxiosResponse>;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function DashboardNotiList({
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<div className="card-header flex items-center justify-between gap-4">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
알림이력
|
||||
|
||||
@@ -45,7 +45,7 @@ export function DashboardNoticeTable() {
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<section className="card card-border m-0 bg-background">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||
<Button type="button" size="sm" color="white" onClick={handleMoreClick}>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function DashboardPage() {
|
||||
const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard();
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 px-[1.8rem] pb-6">
|
||||
<div className="mt-4 flex gap-4 px-[1.8rem] pb-6">
|
||||
<div className="flex-[0.5]">
|
||||
<NationalAsMap />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="card card-border mx-0 flex min-h-[14rem] items-center justify-center bg-background text-muted-foreground">
|
||||
<div className="card card-border m-0 flex min-h-[14rem] items-center justify-center bg-background text-muted-foreground">
|
||||
진행상황을 불러오지 못했습니다.
|
||||
</div>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar
|
||||
return (
|
||||
<div
|
||||
key={slot.docSeComCd}
|
||||
className="card card-border hover:bg-muted/40 mx-0 block bg-background transition-colors"
|
||||
className="card card-border hover:bg-muted/40 m-0 block bg-background transition-colors"
|
||||
>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">{slot.title}</h3>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function NationalAsMap() {
|
||||
const popupColumnSpan = Math.max(1, popupContent.items.length);
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 flex h-full min-h-[40rem] flex-col bg-background">
|
||||
<section className="card card-border m-0 flex h-full min-h-[40rem] flex-col bg-background">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-semibold">전국 행정지원 지도</h3>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
|
||||
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
|
||||
import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: DesignCnstnMngDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
@@ -63,7 +50,7 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
|
||||
thtiDsgn: item.tstmDesignQty,
|
||||
diff: item.tstmDesignQty - item.bgtRfltQty,
|
||||
})),
|
||||
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
|
||||
privacyConsent: "Y",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import type {
|
||||
BzScaleItem,
|
||||
DesignCnstnApplySaveRequestDto,
|
||||
DesignCnstnMngDetResponseDto,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ReviewStepCode } from "../model/view-types";
|
||||
|
||||
@@ -22,22 +21,6 @@ export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: DesignCnstnMngDetResponseDto,
|
||||
prgrsSttsComCd: string,
|
||||
@@ -50,7 +33,7 @@ export function buildSavePayload(
|
||||
return {
|
||||
designCnstnMng: { ...detDto, prgrsSttsComCd },
|
||||
bzScaleList: toBzScaleSaveDto(data.bizSclList ?? []),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(data.esntlFileList ?? []),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { useDesignCnstnMng } from "@/entities/design-cnstn";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { ProgressStatusSection } from "@/widgets/pbadms-sprt";
|
||||
import { AttachmentsViewSection, ProgressStatusSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
@@ -14,7 +14,6 @@ import { getEditableReviewStep } from "../lib/review-step-utils";
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
import { ContactInfoSection } from "./sections/contact-info-section";
|
||||
@@ -91,7 +90,7 @@ function DesignViewPage({ id }: DesignViewPageProps) {
|
||||
<ContactInfoSection det={detDto} />
|
||||
<ElaprDocPrgrsStatus elaprDocNo={detDto.designCnstnDocNo} />
|
||||
<BusinessScaleSection bizSclList={bizSclList} />
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<AttachmentsViewSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/design-cnstn";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.map((file) => (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsSection };
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import type { FnrsCnsltnMngDetResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { mapFnrsSharDetToFormList } from "@/widgets/pbadms-sprt";
|
||||
import { mapFnrsSharDetToFormList, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { FnrsCnsltnRegistFormData } from "./fnrs-cnsltn-regist-schema";
|
||||
import { parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema";
|
||||
@@ -10,20 +10,6 @@ function mapSelectedValue(value?: string | null): "Y" | "N" | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: FnrsCnsltnMngDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsltnRegistFormData {
|
||||
const detDto = data.detDto;
|
||||
if (!detDto) {
|
||||
@@ -94,7 +80,7 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
|
||||
selected: mapSelectedValue(item.rvwRsltAnmlyYn),
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
|
||||
splmntDmndList:
|
||||
splmntDmndList?.map((item) => ({
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type {
|
||||
BizSclTreeDto,
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
@@ -14,6 +12,7 @@ import type {
|
||||
import {
|
||||
mapFnrsSharDetToFormList,
|
||||
type SplmntDmndDialogConfirmPayload,
|
||||
toEsntlAtchFileSaveList,
|
||||
toFnrsSharSaveList,
|
||||
} from "@/widgets/pbadms-sprt";
|
||||
|
||||
@@ -44,22 +43,6 @@ export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
@@ -81,7 +64,7 @@ export function buildSavePayload(
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
|
||||
fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useFnrsCnsltnMng } from "@/entities/fnrs-cnsltn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { AttachmentsViewSection, ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
@@ -15,7 +15,6 @@ import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-check
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
@@ -110,7 +109,7 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
|
||||
formState={checklistFormState}
|
||||
setFormState={setChecklistFormState}
|
||||
/>
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<AttachmentsViewSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
esntlFileList.map((file) => (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsSection };
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IUserInfo } from "@/shared/store/user-store";
|
||||
import type { InstlSncnMngDetResponse } from "@/entities/instl-sncn";
|
||||
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { InstlSncnRegistFormData } from "./instl-sncn-regist-schema";
|
||||
import { parseTaskSeComCdFromApi } from "./instl-sncn-regist-schema";
|
||||
@@ -9,20 +10,6 @@ function mapSelectedValue(value?: string | null): "Y" | "N" | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: InstlSncnMngDetResponse["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnRegistFormData {
|
||||
const { detDto, bizSclList, bizCstList, cklstResultList, esntlFileList, splmntDmndList } = data;
|
||||
|
||||
@@ -84,7 +71,7 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg
|
||||
selected: mapSelectedValue(item.rvwRsltAnmlyYn),
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
|
||||
splmntDmndList:
|
||||
splmntDmndList?.map((item) => ({
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type {
|
||||
BizCstTreeDto,
|
||||
BizSclTreeDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
InstlSncnApplySaveRequestDto,
|
||||
InstlSncnBizCstSaveRequestDto,
|
||||
InstlSncnBzScaleSaveDto,
|
||||
InstlSncnEsntlAtchFileSaveDto,
|
||||
InstlSncnMngDetResponse,
|
||||
InstlSncnSplmntDmndSaveDto,
|
||||
ReviewStepCode,
|
||||
} from "@/entities/instl-sncn";
|
||||
import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState } from "../model/view-types";
|
||||
|
||||
@@ -40,22 +38,6 @@ export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSav
|
||||
}));
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): InstlSncnEsntlAtchFileSaveDto[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: InstlSncnMngDetResponse,
|
||||
checklistFormState: ChecklistFormState,
|
||||
@@ -69,7 +51,7 @@ export function buildSavePayload(
|
||||
bzScaleList: toBzScaleSaveDto(bizSclList),
|
||||
bizCstList: toBizCstSaveDto(bizCstList),
|
||||
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useInstlSncnDetail } from "@/entities/instl-sncn";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { AttachmentsViewSection, ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
@@ -15,7 +15,6 @@ import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-check
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BusinessCostSection } from "./sections/business-cost-section";
|
||||
import { BusinessScaleSection } from "./sections/business-scale-section";
|
||||
@@ -107,7 +106,10 @@ function InstlViewPage({ id }: InstlViewPageProps) {
|
||||
formState={checklistFormState}
|
||||
setFormState={setChecklistFormState}
|
||||
/>
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<AttachmentsViewSection
|
||||
esntlFileList={esntlFileList ?? []}
|
||||
etcFileList={etcFileList ?? []}
|
||||
/>
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
rvwList={rvwList}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/instl-sncn";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
esntlFileList.map((file) => (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsSection };
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils";
|
||||
import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt";
|
||||
|
||||
export interface BizCstMatrixItem {
|
||||
planStpNo: number;
|
||||
@@ -45,9 +45,11 @@ function recalculateBizCstMatrix(
|
||||
if (!item.readonly || !item.children) continue;
|
||||
|
||||
for (const planStpNo of PLAN_STEPS) {
|
||||
const childSum = item.children.reduce(
|
||||
(sum, childCode) => sum + getAmt(next, childCode, planStpNo),
|
||||
0,
|
||||
const childSum = roundAmount(
|
||||
item.children.reduce(
|
||||
(sum, childCode) => sum + getAmt(next, childCode, planStpNo),
|
||||
0,
|
||||
),
|
||||
);
|
||||
const current = getAmt(next, item.code, planStpNo);
|
||||
if (current !== childSum) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CostItemDef } from "@/widgets/pbadms-sprt/lib/business-cost-form-utils";
|
||||
import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import { type BizCstMatrixItem,getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
|
||||
|
||||
@@ -31,11 +31,11 @@ function getBizCstStepTotal(
|
||||
}
|
||||
|
||||
function sumFnrsCstLeaf(item: FnrsCstItem): number {
|
||||
return (
|
||||
return roundAmount(
|
||||
(item.gvexpAmt ?? 0) +
|
||||
(item.lcexpAmt ?? 0) +
|
||||
(item.orgprBdchgAmt ?? 0) +
|
||||
(item.etcAmt ?? 0)
|
||||
(item.lcexpAmt ?? 0) +
|
||||
(item.orgprBdchgAmt ?? 0) +
|
||||
(item.etcAmt ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function calcFnrsCstRemaining(
|
||||
const bizTotal = getBizCstStepTotal(bizCstList, costItems, planStpNo);
|
||||
const fnrsItem = fnrsCstList.find((item) => item.planStpNo === planStpNo);
|
||||
const sharTotal = fnrsItem ? sumFnrsCstLeaf(fnrsItem) : 0;
|
||||
return { planStpNo, remaining: bizTotal - sharTotal };
|
||||
return { planStpNo, remaining: roundAmount(bizTotal - sharTotal) };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
SewerBscPlanDetResponseDto,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan";
|
||||
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
|
||||
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||
@@ -26,20 +27,6 @@ function mapSelectedValue(value?: string | null): "Y" | "N" | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapEsntlAtchFileList(esntlFileList: SewerBscPlanDetResponseDto["esntlFileList"]) {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
function createEmptyTmplntDtlList(swtrmtFcltCd: string) {
|
||||
return TMPLNT_DTL_STEPS.map((planStpNo) => ({
|
||||
planStpNo,
|
||||
@@ -218,7 +205,7 @@ export function mapDetailToFormData(data: SewerBscPlanDetResponseDto): SewerBscP
|
||||
selected: mapSelectedValue(item.rvwRsltAnmlyYn),
|
||||
picRvwCn: item.picRvwCn,
|
||||
})) ?? [],
|
||||
esntlAtchFileList: mapEsntlAtchFileList(data.esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
|
||||
splmntDmndList:
|
||||
data.splmntDmndList?.map((item) => ({
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type {
|
||||
EsntlAtchFileItem,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
SewerBscPlanApplySaveRequestDto,
|
||||
SewerBscPlanDetResponseDto,
|
||||
SewerBscPlanSplmntDmndItem,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
|
||||
import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
|
||||
|
||||
import type { ChecklistFormState, ReviewStepCode } from "../model/view-types";
|
||||
|
||||
@@ -13,22 +11,6 @@ import { toCklstSaveDto } from "./checklist-utils";
|
||||
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
|
||||
import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils";
|
||||
|
||||
export function toEsntlAtchFileSaveDto(
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList) {
|
||||
if (!item.esntlAtchFileId || !item.atflCd) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, item.atflCd);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSavePayload(
|
||||
data: SewerBscPlanDetResponseDto,
|
||||
checklistFormState: ChecklistFormState,
|
||||
@@ -116,7 +98,7 @@ export function buildSavePayload(
|
||||
})),
|
||||
})),
|
||||
cklstResultList: toCklstSaveDto(cklstResultRows, checklistFormState, reviewStep),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList),
|
||||
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
|
||||
import type {
|
||||
AtflMngClientResponseDto,
|
||||
EsntlAtchFileMngTreeResponseDto,
|
||||
} from "@/entities/sewer-bsc-plan";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
|
||||
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
|
||||
조회된 필수 첨부파일 항목이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
esntlFileList.map((file) => (
|
||||
<TableRow key={file.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{file.esntlAtchFileNm}
|
||||
{file.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
|
||||
등록된 기타첨부파일이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="cursor-pointer text-blue-600 hover:underline"
|
||||
onClick={() => downloadAttachment(file.atflCd)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsSection };
|
||||
@@ -154,8 +154,7 @@ function FnrsCstFormSection() {
|
||||
<TableCell key={planStpNo}>
|
||||
<Field>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
type="number"
|
||||
className={cn("text-right", row.readonly && "bg-muted")}
|
||||
readOnly={row.readonly}
|
||||
disabled={row.readonly}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { ElaprDocPrgrsStatus } from "@/entities/elapr";
|
||||
import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw";
|
||||
import { useSewerBscPlanDetails } from "@/entities/sewer-bsc-plan";
|
||||
import { ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { AttachmentsViewSection, ProgressStatusSection, SupplementDemandSection } from "@/widgets/pbadms-sprt";
|
||||
import { Skeleton } from "@common/components";
|
||||
|
||||
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
|
||||
@@ -15,7 +15,6 @@ import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-check
|
||||
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
|
||||
|
||||
import { FloatingActionButtons } from "./components/floating-action-buttons";
|
||||
import { AttachmentsSection } from "./sections/attachments-section";
|
||||
import { BasicInfoSection } from "./sections/basic-info-section";
|
||||
import { BizCstMatrixSection } from "./sections/biz-cst-matrix-section";
|
||||
import { ChecklistSection } from "./sections/checklist-section";
|
||||
@@ -116,7 +115,7 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
|
||||
formState={checklistFormState}
|
||||
setFormState={setChecklistFormState}
|
||||
/>
|
||||
<AttachmentsSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<AttachmentsViewSection esntlFileList={esntlFileList} etcFileList={etcFileList} />
|
||||
<SupplementDemandSection splmntDmndList={splmntDmndList} />
|
||||
<TechReviewOpinionSection
|
||||
ref={techReviewOpinionRef}
|
||||
|
||||
@@ -3,6 +3,7 @@ export const ENDPOINT = {
|
||||
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
|
||||
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
|
||||
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
|
||||
DOWNLOAD_BY_ATFL_ID: "/attach-service/api/v1/download",
|
||||
},
|
||||
AS_SERVICE: {
|
||||
ACNTG_SE: "/as-service/api/v1/acntg-se",
|
||||
|
||||
@@ -108,22 +108,8 @@ export interface EsntlAtchFileMngTreeResponseDto {
|
||||
esntlAtchYn: string;
|
||||
/** 필수첨부파일비고 */
|
||||
esntlAtchFileRmrk: string | null;
|
||||
/** 첨부파일 코드 */
|
||||
atflCd?: string;
|
||||
/** 첨부파일 일련번호 */
|
||||
atflSn?: number;
|
||||
/** 첨부파일 ID */
|
||||
atflId?: string;
|
||||
/** 원본 파일명 */
|
||||
orgnlFileNm?: string;
|
||||
/** 첨부파일 크기 */
|
||||
atflFlieSz?: number;
|
||||
/** 첨부파일 타입 */
|
||||
atflType?: string;
|
||||
/** 다운로드 횟수 */
|
||||
dwnldNocs?: number;
|
||||
/** 등록일시 */
|
||||
regDt?: string;
|
||||
/** 첨부파일 목록 (슬롯당 atchFileGroupId 1:N) */
|
||||
atchFileList?: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
/** 첨부파일 응답 DTO (attach-service client) */
|
||||
|
||||
@@ -2,8 +2,10 @@ export {
|
||||
atchFileGroupIdPath,
|
||||
createEtcUploadKey,
|
||||
createUploadKey,
|
||||
getEsntlAtchFileGroupId,
|
||||
isEtcFileKey,
|
||||
isFormEsntlAtchComplete,
|
||||
toEsntlAtchFileSaveList,
|
||||
} from "./lib/attachments-form-utils";
|
||||
export {
|
||||
buildAcntgCostItems,
|
||||
@@ -28,8 +30,9 @@ export {
|
||||
toFnrsSharSaveList,
|
||||
validateBizCstSharBalance,
|
||||
} from "./lib/fnrs-shar-form-utils";
|
||||
export { calcDiffPercent, fmtNumber, indentPx } from "./lib/format-utils";
|
||||
export { calcDiffPercent, fmtNumber, indentPx, roundAmount } from "./lib/format-utils";
|
||||
export { AttachmentsFormSection } from "./ui/attachments-form-section";
|
||||
export { AttachmentsViewSection } from "./ui/attachments-view-section";
|
||||
export { BusinessCostFormSection } from "./ui/business-cost-form-section";
|
||||
export { BusinessScaleFormSection } from "./ui/business-scale-form-section";
|
||||
export { ChecklistFormSection } from "./ui/checklist-form-section";
|
||||
|
||||
@@ -10,7 +10,7 @@ export type FieldPrefix =
|
||||
|
||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||
|
||||
interface EsntlAtchFileListItem {
|
||||
export interface EsntlAtchFileListItem {
|
||||
esntlAtchFileId: string;
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
@@ -42,6 +42,27 @@ export function toFileAccept(rmrk: string | null | undefined): string {
|
||||
.join(",");
|
||||
}
|
||||
|
||||
export function getEsntlAtchFileGroupId(item: EsntlAtchFileMngTreeResponseDto): string {
|
||||
return item.atchFileList?.[0]?.atflCd ?? "";
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveList(
|
||||
esntlFileList?: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileListItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
const atchFileGroupId = getEsntlAtchFileGroupId(item);
|
||||
if (!item.esntlAtchFileId || !atchFileGroupId) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, atchFileGroupId);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function isFormEsntlAtchComplete(
|
||||
essentialFiles: EsntlAtchFileMngTreeResponseDto[],
|
||||
esntlAtchFileList: EsntlAtchFileListItem[],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AcntgSeMngTreeDto } from "@/entities/acntg";
|
||||
|
||||
import { roundAmount } from "./format-utils";
|
||||
|
||||
export interface CostItemDef {
|
||||
code: string;
|
||||
label: string;
|
||||
@@ -29,7 +31,11 @@ function sumChildAmounts(list: BizCstListItem[], childCodes: string[]) {
|
||||
}
|
||||
});
|
||||
|
||||
return { bscPlanAmt, bgtRfltAmt, tstmDesignAmt };
|
||||
return {
|
||||
bscPlanAmt: roundAmount(bscPlanAmt),
|
||||
bgtRfltAmt: roundAmount(bgtRfltAmt),
|
||||
tstmDesignAmt: roundAmount(tstmDesignAmt),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAcntgCostItems(tree: AcntgSeMngTreeDto[]): CostItemDef[] {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CnstpSeMngTreeDto } from "@/entities/cnstp";
|
||||
|
||||
import { roundAmount } from "./format-utils";
|
||||
|
||||
export interface ScaleItemDef {
|
||||
code: string;
|
||||
label: string;
|
||||
@@ -30,13 +32,17 @@ function sumChildScale(list: BzScaleListItem[], childCodes: string[]) {
|
||||
}
|
||||
});
|
||||
|
||||
return { bscPl, budgetReflct, thtiDsgn };
|
||||
return {
|
||||
bscPl: roundAmount(bscPl),
|
||||
budgetReflct: roundAmount(budgetReflct),
|
||||
thtiDsgn: roundAmount(thtiDsgn),
|
||||
};
|
||||
}
|
||||
|
||||
function withDiff(item: BzScaleListItem): BzScaleListItem {
|
||||
const budgetReflct = item.budgetReflct || 0;
|
||||
const thtiDsgn = item.thtiDsgn || 0;
|
||||
return { ...item, diff: thtiDsgn - budgetReflct };
|
||||
return { ...item, diff: roundAmount(thtiDsgn - budgetReflct) };
|
||||
}
|
||||
|
||||
export function buildCnstpScaleItems(tree: CnstpSeMngTreeDto[]): ScaleItemDef[] {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FnrsSharItem } from "@/entities/fnrs-cnsltn";
|
||||
import type { FnrsSharBizCstTreeDto, FnrsSharSeMngTreeDto } from "@/entities/fnrs-shar-se";
|
||||
|
||||
import type { BizCstListItem, CostItemDef } from "./business-cost-form-utils";
|
||||
import { roundAmount } from "./format-utils";
|
||||
|
||||
export interface FnrsSharItemDef {
|
||||
code: string;
|
||||
@@ -78,7 +79,10 @@ function sumChildSharAmounts(list: FnrsSharListItem[], childCodes: string[]) {
|
||||
chgRqtAmt += child.chgRqtAmt || 0;
|
||||
});
|
||||
|
||||
return { exstnAmt, chgRqtAmt };
|
||||
return {
|
||||
exstnAmt: roundAmount(exstnAmt),
|
||||
chgRqtAmt: roundAmount(chgRqtAmt),
|
||||
};
|
||||
}
|
||||
|
||||
export function recalculateFnrsSharTotals(
|
||||
@@ -103,7 +107,7 @@ export function recalculateFnrsSharTotals(
|
||||
...current,
|
||||
exstnAmt,
|
||||
chgRqtAmt,
|
||||
ajmtAmt: exstnAmt - chgRqtAmt,
|
||||
ajmtAmt: roundAmount(exstnAmt - chgRqtAmt),
|
||||
};
|
||||
hasChanges = true;
|
||||
}
|
||||
@@ -158,7 +162,10 @@ export function sumFnrsSharLeafAmounts(
|
||||
chgRqtAmt += row.chgRqtAmt || 0;
|
||||
});
|
||||
|
||||
return { exstnAmt, chgRqtAmt };
|
||||
return {
|
||||
exstnAmt: roundAmount(exstnAmt),
|
||||
chgRqtAmt: roundAmount(chgRqtAmt),
|
||||
};
|
||||
}
|
||||
|
||||
export function calcBizCstSharRemaining(
|
||||
@@ -171,8 +178,8 @@ export function calcBizCstSharRemaining(
|
||||
const leafSum = sumFnrsSharLeafAmounts(sharList, sharItems);
|
||||
|
||||
return {
|
||||
exstnRemaining: bizCstTotal.bscPlanAmt - leafSum.exstnAmt,
|
||||
chgRqtRemaining: bizCstTotal.tstmDesignAmt - leafSum.chgRqtAmt,
|
||||
exstnRemaining: roundAmount(bizCstTotal.bscPlanAmt - leafSum.exstnAmt),
|
||||
chgRqtRemaining: roundAmount(bizCstTotal.tstmDesignAmt - leafSum.chgRqtAmt),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,7 +208,7 @@ export function toFnrsSharSaveList(list: FnrsSharListItem[]): FnrsSharItem[] {
|
||||
fnrsSharSeCd: row.fnrsSharSeCd,
|
||||
exstnAmt: row.exstnAmt || 0,
|
||||
chgRqtAmt: row.chgRqtAmt || 0,
|
||||
ajmtAmt: (row.exstnAmt || 0) - (row.chgRqtAmt || 0),
|
||||
ajmtAmt: roundAmount((row.exstnAmt || 0) - (row.chgRqtAmt || 0)),
|
||||
rmrkCn: row.rmrkCn,
|
||||
}));
|
||||
}
|
||||
@@ -239,7 +246,7 @@ export function mapFnrsSharDetToFormList(tree: FnrsSharBizCstTreeDto[]): FnrsSha
|
||||
fnrsSharSeCd: item.code,
|
||||
exstnAmt,
|
||||
chgRqtAmt,
|
||||
ajmtAmt: exstnAmt - chgRqtAmt,
|
||||
ajmtAmt: roundAmount(exstnAmt - chgRqtAmt),
|
||||
rmrkCn: node?.rmrkCn ?? "",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -18,6 +18,13 @@ export function indentPx(indNm: string | undefined): number {
|
||||
return (spaces / 4) * 16;
|
||||
}
|
||||
|
||||
const AMOUNT_DECIMAL_PLACES = 2;
|
||||
|
||||
export function roundAmount(value: number): number {
|
||||
const factor = 10 ** AMOUNT_DECIMAL_PLACES;
|
||||
return Math.round((value + Number.EPSILON) * factor) / factor;
|
||||
}
|
||||
|
||||
export function calcDiffPercent(diff: number, baseline: number): string {
|
||||
if (baseline === 0) return "-";
|
||||
return `${Math.round((diff / baseline) * 100)}%`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Paperclip, X } from "lucide-react";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useTus } from "@/shared/lib";
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
type FieldPrefix,
|
||||
isEtcFileKey,
|
||||
toFileAccept,
|
||||
} from "../lib/attachments-form-utils";
|
||||
} from "../lib/attachments-form-utils";
|
||||
|
||||
interface AttachmentsFormData {
|
||||
esntlAtchFileList: Array<{
|
||||
@@ -103,22 +104,22 @@ function AttachmentsFormSection({
|
||||
if (initialEsntlFiles?.length) {
|
||||
const nextUploadedFiles: Record<string, UploadedFileItem[]> = {};
|
||||
|
||||
for (const file of initialEsntlFiles) {
|
||||
if (!file.esntlAtchFileId || !file.atflCd || !file.atflId) continue;
|
||||
for (const slot of initialEsntlFiles) {
|
||||
if (!slot.esntlAtchFileId) continue;
|
||||
|
||||
const fileKey = `server::${file.esntlAtchFileId}::${file.atflId}`;
|
||||
const fileName = file.orgnlFileNm ?? "첨부파일";
|
||||
const files = (slot.atchFileList ?? []).filter((file) => file.atflId && file.atflCd);
|
||||
if (files.length === 0) continue;
|
||||
|
||||
nextUploadedFiles[file.esntlAtchFileId] = [
|
||||
...(nextUploadedFiles[file.esntlAtchFileId] ?? []),
|
||||
{
|
||||
fileName,
|
||||
fileKey,
|
||||
atflId: file.atflId,
|
||||
atflCd: file.atflCd,
|
||||
},
|
||||
];
|
||||
atflCdBySlotRef.current[file.esntlAtchFileId] = file.atflCd;
|
||||
const [firstFile] = files;
|
||||
if (!firstFile) continue;
|
||||
|
||||
nextUploadedFiles[slot.esntlAtchFileId] = files.map((file) => ({
|
||||
fileName: file.orgnlFileNm ?? "첨부파일",
|
||||
fileKey: `server::${slot.esntlAtchFileId}::${file.atflId}`,
|
||||
atflId: file.atflId,
|
||||
atflCd: file.atflCd,
|
||||
}));
|
||||
atflCdBySlotRef.current[slot.esntlAtchFileId] = firstFile.atflCd;
|
||||
}
|
||||
|
||||
setUploadedFiles(nextUploadedFiles);
|
||||
@@ -372,7 +373,7 @@ function AttachmentsFormSection({
|
||||
render={() => (
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
@@ -406,36 +407,43 @@ function AttachmentsFormSection({
|
||||
</TableHead>
|
||||
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
|
||||
<TableCell className="space-y-2">
|
||||
{uploadedFiles[file.esntlAtchFileId]?.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{uploadedFiles[file.esntlAtchFileId]?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{uploadedFiles[file.esntlAtchFileId].map((item, idx) => {
|
||||
const uploading = isUploading[item.fileKey] ?? false;
|
||||
|
||||
return (
|
||||
<div key={item.fileKey} className="flex items-center gap-2">
|
||||
<div
|
||||
key={item.fileKey}
|
||||
className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent p-2"
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{item.file?.name ?? item.fileName}
|
||||
</TextShimmer>
|
||||
{!uploading && (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
<X
|
||||
className="size-5 shrink-0 text-destructive"
|
||||
aria-label="삭제"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
|
||||
onChange={(e) => {
|
||||
@@ -447,10 +455,10 @@ function AttachmentsFormSection({
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<span>파일선택</span>
|
||||
<span>파일추가</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
@@ -475,35 +483,35 @@ function AttachmentsFormSection({
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<span>파일 추가</span>
|
||||
<span>파일추가</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rounded border p-4">
|
||||
{etcFiles.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{etcFiles.map((item, idx) => {
|
||||
const uploading = isUploading[item.fileKey] ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.fileKey}
|
||||
className="flex items-center justify-between rounded-sm border p-2"
|
||||
className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent p-2"
|
||||
>
|
||||
<TextShimmer className="px-2 text-xl" active={uploading}>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{item.file?.name ?? item.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="sm"
|
||||
color="gray"
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEtcFile(idx);
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<X className="size-5 shrink-0 text-destructive" aria-label="삭제" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsViewSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewSectionProps) {
|
||||
const { mutate: downloadByAtflCd } = useDownloadAttachmentByAtflCd();
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.map((slot) => {
|
||||
const files = slot.atchFileList ?? [];
|
||||
|
||||
return (
|
||||
<TableRow key={slot.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{slot.esntlAtchFileNm}
|
||||
{slot.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{files.length === 0 ? (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{files.map((file) => (
|
||||
<a
|
||||
key={file.atflId}
|
||||
className="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadByAtflId(file.atflId)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0 text-blue-600" />
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{files.length === 0 ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{files.map((file) => (
|
||||
<div key={file.atflId}>{formatFileSize(file.atflFlieSz)}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadByAtflCd(file.atflCd)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0 text-blue-600" />
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsViewSection };
|
||||
Reference in New Issue
Block a user