Merge branch 'iipbs/as/dev'

This commit is contained in:
2026-09-18 17:37:31 +09:00
21 changed files with 283 additions and 155 deletions
+4
View File
@@ -6,3 +6,7 @@ export {
} from "./api/queries";
export { parseFilenameFromResponse } from "./lib/parse-filename-from-response";
export type { Attach, AttachService } from "./model/attach-interface";
export {
AttachmentDownloadLink,
type AttachmentDownloadLinkProps,
} from "./ui/attachment-download-link";
@@ -0,0 +1,54 @@
"use client";
import { Paperclip } from "lucide-react";
import { cn } from "@common/components";
import { useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "../api/queries";
interface AttachmentDownloadLinkProps {
atflCd?: string;
atflId?: string;
fileName?: string;
className?: string;
}
function AttachmentDownloadLink({
atflCd,
atflId,
fileName,
className,
}: AttachmentDownloadLinkProps) {
const downloadByAtflCd = useDownloadAttachmentByAtflCd();
const downloadByAtflId = useDownloadAttachmentByAtflId();
const displayName = fileName || "첨부파일";
const canDownload = Boolean(atflId || atflCd);
const isPending = atflId ? downloadByAtflId.isPending : downloadByAtflCd.isPending;
function handleClick() {
if (atflId) {
downloadByAtflId.mutate(atflId);
return;
}
if (atflCd) downloadByAtflCd.mutate(atflCd);
}
return (
<button
type="button"
className={cn(
"inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline disabled:cursor-wait disabled:opacity-60",
className,
)}
aria-label={`${displayName} 다운로드`}
disabled={isPending || !canDownload}
onClick={handleClick}
>
<Paperclip className="my-1 size-5 shrink-0" />
{displayName}
</button>
);
}
export { AttachmentDownloadLink };
export type { AttachmentDownloadLinkProps };
@@ -90,6 +90,7 @@ function ElaprAplnPickerGrid({
? isExternalLoading
: aplnQuery.isPending && Boolean(elaprTmpltId);
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
const excludedVisibleRows = visibleRows.filter((r) => r.atrzSeComCd !== "ATRZ01");
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
@@ -149,7 +150,7 @@ function ElaprAplnPickerGrid({
</div>
) : !isReady ? (
<p className="py-8 text-center text-lg text-muted-foreground">결재서식을 선택하세요.</p>
) : visibleRows.length === 0 ? (
) : excludedVisibleRows.length === 0 ? (
<p className="py-16 text-center text-lg text-muted-foreground">
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
</p>
@@ -174,10 +175,10 @@ function ElaprAplnPickerGrid({
</TableHeader>
<TableBody>
<SortableContext
items={visibleRows.map((r) => r.rowKey)}
items={excludedVisibleRows.map((r) => r.rowKey)}
strategy={verticalListSortingStrategy}
>
{visibleRows.map((row, index) => (
{excludedVisibleRows.map((row, index) => (
<SortableAplnPickerRow
key={row.rowKey}
row={row}
@@ -2,14 +2,11 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
import {
Button,
@@ -319,15 +316,10 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank"
rel="noreferrer"
>
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"}
</a>
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import { CodeSelect } from "@/entities/code";
import type { EsntlAtchFileMng } from "@/entities/esntl";
import {
@@ -61,6 +61,7 @@ function EsntlAtchFileFormDialog({
const formId = useId();
const atchFileGroupIdRef = useRef("");
const [uploadedFiles, setUploadedFiles] = useState<UploadedTemplateFile[]>([]);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { register, handleSubmit, reset, setValue, watch } = useForm<EsntlAtchFileFormValues>({
defaultValues: {
@@ -219,6 +220,9 @@ function EsntlAtchFileFormDialog({
files={uploadedFiles.map((item) => ({
fileKey: item.fileKey,
fileName: item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={fileInputId}
isUploadingByKey={isUploading}
@@ -6,6 +6,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
rjct: "rjct",
sncnAprv: "sncnAprv",
rvwRply: "rvwRply",
rvwOpnnRply: "rvwOpnnRply",
rqstTechRvw: "rqstTechRvw",
fileSplmnt: "fileSplmnt",
dsgntRvwr: "dsgntRvwr",
@@ -37,6 +38,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
],
RB: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn],
RA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
ROLE_USER04: {
@@ -45,7 +47,11 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
},
ROLE_USER03: {
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
TP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
TP: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
],
RA: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
@@ -54,7 +60,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "EV", "TE", "RK"] as const;
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "EV", "SL", "TE", "RK"] as const;
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
string,
@@ -63,8 +69,11 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
ER: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
},
EV: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
SL: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
TE: {
ATRZ02: [
@@ -79,7 +88,13 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
],
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply],
ATRZ99: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
],
},
EV: {
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
},
RK: {
ATRZ02: [
@@ -143,7 +158,7 @@ function resolveRoleUser01AtrzButtonActions(
if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false;
if (options?.hasCompletedDraft != null) {
if (prgrsSttsComCd === "TE" || prgrsSttsComCd === "RK") {
if (prgrsSttsComCd === "SL" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "RK") {
if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
return options.hasCompletedDraft;
}
@@ -114,7 +114,8 @@ function FloatingActionButtons({
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
const needsElaprPrgrsCheck = needsAtrzResolve && ["TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkDraftCheck = dtlInstCd === "ROLE_USER01" && ["TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkDraftCheck =
dtlInstCd === "ROLE_USER01" && ["SL", "TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkForRb = dtlInstCd === "ROLE_USER05" && prgrsSttsComCd === "RB";
const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb;
@@ -137,6 +138,7 @@ function FloatingActionButtons({
elaprPrgrsForPending,
userInfo?.userUnqId,
);
const viewOnlyPwrkCnsltnDoc = prgrsSttsComCd === "SL";
const actionButtons = useMemo(() => {
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
@@ -317,12 +319,11 @@ function FloatingActionButtons({
return;
}
const isResultReply = prgrsSttsComCd === "RR";
const payload = buildTechReviewReplyPayload(
data,
checklistFormState,
"ENV",
isResultReply ? "EP" : "RB",
"EP",
"CP",
getReviewOpinionFormData(),
opnrNm,
@@ -336,6 +337,36 @@ function FloatingActionButtons({
techReviewOpinionRef?.current?.resetForm();
}
async function handleRvwOpnnRplyClick() {
const { percent } = computeReviewStepChecklistProgress(
data.cklstResultList ?? [],
checklistFormState.results,
"ENV",
);
if (percent !== 100) {
alert("모든 체크리스트 항목을 체크해야 검토 의견 회신이 가능합니다.");
return;
}
const payload = buildTechReviewReplyPayload(
data,
checklistFormState,
"ENV",
"RB",
"CP",
getReviewOpinionFormData(),
opnrNm,
);
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
request: payload,
successMessage: "검토 의견 회신이 완료되었습니다.",
});
techReviewOpinionRef?.current?.resetForm();
}
async function handleRqstTechRvwClick() {
if (prgrsSttsComCd === "LE") {
const { percent } = computeReviewStepChecklistProgress(
@@ -365,7 +396,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
request: payload,
successMessage: "조치결과 검토의뢰가 완료되었습니다.",
successMessage: "조치 결과 검토 의뢰가 완료되었습니다.",
});
}
@@ -566,10 +597,15 @@ function FloatingActionButtons({
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv) && (
<Button onClick={handleSncnAprvClick} disabled={isProcessing}>
<Button color="green" onClick={handleSncnAprvClick} disabled={isProcessing}>
결과 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply) && (
<Button onClick={handleRvwOpnnRplyClick} disabled={isProcessing}>
검토 의견 회신
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
검토 결과 회신
@@ -582,7 +618,7 @@ function FloatingActionButtons({
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw) && (
<Button onClick={handleRqstActnRvwClick} disabled={isProcessing}>
조치결과 검토 의뢰
조치 결과 검토 의뢰
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && (
@@ -698,7 +734,11 @@ function FloatingActionButtons({
fnrsCnsltnDocNo={docNo}
atrzSeComCdList={atrzSeComCdList}
prgrsSttsComCd={prgrsSttsComCd}
canSave={needsElaprPrgrsCheck && !hasPendingApprovalRequest ? false : undefined}
canSave={
(needsElaprPrgrsCheck && !hasPendingApprovalRequest) || viewOnlyPwrkCnsltnDoc
? false
: null
}
onClose={() => setActiveDialog(null)}
/>
@@ -837,6 +877,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
rjct: handleChangePrgrsSttsComCd,
sncnAprv: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
rvwOpnnRply: handleTechReview,
rqstTechRvw: handleRequestExamine,
fileSplmnt: handleDmndSplmnt,
dsgntRvwr: handleDsgntRvwr,
@@ -8,7 +8,7 @@ import { PanelRightClose, PanelRightOpen } from "lucide-react";
import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib";
import { AttachmentFileField } from "@/shared/ui";
import { attachService, parseFilenameFromResponse } from "@/entities/attach";
import { attachService, parseFilenameFromResponse, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
pwrkCnsltnDocCrtRsltSaveRequestSchema,
pwrkCnsltnDocCrtRsltService,
@@ -43,14 +43,14 @@ const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::";
const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["RA", "RR", "EP", "TA"]);
const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["TP", "RA", "RR", "EP", "TA"]);
interface PwrkCnsltnDocMngDialogProps {
open: boolean;
fnrsCnsltnDocNo: string;
atrzSeComCdList?: string[];
prgrsSttsComCd?: string;
canSave?: boolean;
canSave?: boolean | null;
onClose: () => void;
}
@@ -78,6 +78,7 @@ function PwrkCnsltnDocMngDialog({
const [atchFileGroupId, setAtchFileGroupId] = useState("");
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const {
data: pwrkCnsltnData,
@@ -344,6 +345,9 @@ function PwrkCnsltnDocMngDialog({
files={uploadedFiles.map((item) => ({
fileKey: item.fileKey,
fileName: item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId="pwrk-cnsltn-doc-atch-file-input"
isUploadingByKey={isUploading}
@@ -43,11 +43,21 @@ function PwrkCnsltnDocPjtcoAjmtTable({
return (
<Table variant="secondary">
<colgroup>
<col width="30%" />
<col width="50%" />
<col width="20%" />
</colgroup>
<TableHeader className="sticky top-0">
<TableRow>
<TableHead>구분</TableHead>
<TableHead>검증결과</TableHead>
<TableHead className="text-right">검증금액</TableHead>
<TableHead className="text-right">
<div className="flex flex-col gap-1">
<span>검증금액</span>
<span className="text-sm">(단위: 백만 원)</span>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -332,24 +332,26 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
)}
</>
) : (
<Button
type="button"
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완 완료
</Button>
<>
{fnrsCnsltnDocNo ? (
<Button
color="orange"
type="button"
onClick={() => setIsPwrkCnsltnDocMngOpen(true)}
disabled={isSubmitting}
>
실무협의서 관리
</Button>
) : null}
<Button
type="button"
onClick={form.handleSubmit(onSplmntCmptn)}
disabled={isSubmitting}
>
보완 완료
</Button>
</>
)}
{isSplmntPrgrsStts && fnrsCnsltnDocNo ? (
<Button
color="orange"
type="button"
onClick={() => setIsPwrkCnsltnDocMngOpen(true)}
disabled={isSubmitting}
>
실무협의서 관리
</Button>
) : null}
</div>
</div>
</div>
@@ -2,13 +2,14 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
import {
RadioGroup,
@@ -82,6 +83,7 @@ const TechReviewOpinionSection = forwardRef<
opnrSeComCd: editableReviewStep ?? "",
});
const [uploadedFile, setUploadedFile] = useState<UploadedOpinionFile | null>(null);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
useEffect(() => {
setFormData((prev) => ({
@@ -246,7 +248,15 @@ const TechReviewOpinionSection = forwardRef<
<AttachmentFileField
files={
uploadedFile
? [{ fileKey: uploadedFile.fileKey, fileName: uploadedFile.fileName }]
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: []
}
inputId={`rvw-opnn-${reviewStep}-file-input`}
@@ -320,15 +330,10 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank"
rel="noreferrer"
>
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"}
</a>
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
@@ -2,14 +2,11 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { InstlSncnRvwOpnnResponseDto, ReviewStepCode } from "@/entities/instl-sncn";
import {
Button,
@@ -331,15 +328,10 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank"
rel="noreferrer"
>
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"}
</a>
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
@@ -2,14 +2,11 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { SewerBscPlanRvwOpnnResponseDto } from "@/entities/sewer-bsc-plan";
import {
Button,
@@ -338,15 +335,10 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
</TableCell>
<TableCell align="center">
{item.atchFileInfo?.atflCd ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank"
rel="noreferrer"
>
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"}
</a>
<AttachmentDownloadLink
atflCd={item.atchFileInfo.atflCd}
fileName={item.atchFileInfo.orgnlFileNm || "다운로드"}
/>
) : (
"-"
)}
@@ -2,10 +2,8 @@
import { useMemo, useState } from "react";
import { Paperclip } from "lucide-react";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { AttachmentDownloadLink } from "@/entities/attach";
import { useFacilityList } from "@/entities/bns-if";
import { useNowDmgMtncGnsvy } from "@/entities/dc";
import type {
@@ -176,7 +174,6 @@ function TmplntFacilityMatrix({
}
function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
const [selectedCode, setSelectedCode] = useState(tmplntList[0]?.swtrmtFcltCd ?? "");
const { data: facilityListData } = useFacilityList(
{ sido: det.sido ?? "", gugun: det.gugun ?? "" },
@@ -261,13 +258,11 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
<TableCell>{facilityName} 계획평면도</TableCell>
<TableCell>
{item.atchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.atchFileGroupId ?? "")}
>
<Paperclip className="size-5 shrink-0" />
{item.atchFileInfo?.orgnlFileNm || "다운로드"}
</a>
<AttachmentDownloadLink
atflCd={item.atchFileGroupId}
fileName={item.atchFileInfo?.orgnlFileNm || "다운로드"}
className="items-center"
/>
) : (
"-"
)}
@@ -6,6 +6,7 @@ import { UploadedFileChip } from "./uploaded-file-chip";
interface AttachmentFileFieldItem {
fileKey: string;
fileName: string;
onDownload?: () => void;
}
interface AttachmentFileFieldProps {
@@ -41,6 +42,7 @@ function AttachmentFileField({
fileName={file.fileName}
isUploading={isUploadingByKey[file.fileKey] ?? false}
onRemove={onRemove ? () => onRemove(index) : undefined}
onDownload={file.onDownload}
/>
))}
</div>
+26 -5
View File
@@ -8,15 +8,36 @@ interface UploadedFileChipProps {
fileName: string;
isUploading?: boolean;
onRemove?: () => void;
onDownload?: () => void;
}
function UploadedFileChip({ fileName, isUploading = false, onRemove }: UploadedFileChipProps) {
function UploadedFileChip({
fileName,
isUploading = false,
onRemove,
onDownload,
}: UploadedFileChipProps) {
const canDownload = Boolean(onDownload) && !isUploading;
return (
<div className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent p-2">
<div className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent px-3 py-2">
<Paperclip className="size-5 shrink-0" />
<TextShimmer className="text-xl" active={isUploading}>
{fileName}
</TextShimmer>
{canDownload ? (
<button
type="button"
className="cursor-pointer text-left hover:underline"
aria-label={`${fileName} 다운로드`}
onClick={onDownload}
>
<TextShimmer className="text-xl" active={isUploading}>
{fileName}
</TextShimmer>
</button>
) : (
<TextShimmer className="text-xl" active={isUploading}>
{fileName}
</TextShimmer>
)}
{onRemove ? (
<button
type="button"
@@ -15,7 +15,7 @@ import {
SectionCollapsible,
UploadedFileChip,
} from "@/shared/ui";
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { attachService, useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "@/entities/attach";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
Button,
@@ -217,6 +217,7 @@ function AttachmentsFormSection({
{ docSeComCd },
);
const { mutate: downloadTemplate } = useDownloadAttachmentByAtflCd();
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
async function invalidateAttachmentQueries() {
await queryClient.invalidateQueries({
@@ -431,6 +432,9 @@ function AttachmentsFormSection({
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
fileKey: item.fileKey,
fileName: item.file?.name ?? item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={`${file.esntlAtchFileId}-file-input`}
isUploadingByKey={isUploading}
@@ -470,6 +474,9 @@ function AttachmentsFormSection({
fileName={item.file?.name ?? item.fileName}
isUploading={isUploading[item.fileKey] ?? false}
onRemove={() => void removeEtcFile(idx)}
onDownload={
item.atflId ? () => downloadByAtflId(item.atflId!) : undefined
}
/>
))}
</div>
@@ -1,11 +1,9 @@
"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 { AttachmentDownloadLink } from "@/entities/attach";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface AttachmentsViewSectionProps {
@@ -14,9 +12,6 @@ interface AttachmentsViewSectionProps {
}
function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewSectionProps) {
const { mutate: downloadByAtflCd } = useDownloadAttachmentByAtflCd();
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
return (
<>
{esntlFileList.length > 0 && (
@@ -48,14 +43,12 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
) : (
<div className="space-y-2">
{files.map((file) => (
<a
<AttachmentDownloadLink
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>
atflId={file.atflId}
fileName={file.orgnlFileNm || "다운로드"}
className="items-center"
/>
))}
</div>
)}
@@ -95,13 +88,11 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
<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>
<AttachmentDownloadLink
atflCd={file.atflCd}
fileName={file.orgnlFileNm || file.physFileNm || file.atflId}
className="items-center"
/>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
@@ -4,7 +4,7 @@ import { useEffect, useId, useState } from "react";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import { CodeSelect } from "@/entities/code";
import {
Button,
@@ -96,6 +96,7 @@ function SplmntDmndDialog({
}: SplmntDmndDialogProps) {
const formId = useId();
const [rows, setRows] = useState<SplmntDmndRow[]>([createEmptyRow()]);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
@@ -289,6 +290,9 @@ function SplmntDmndDialog({
files={row.uploadedFiles.map((file) => ({
fileKey: file.fileKey,
fileName: file.fileName,
onDownload: file.atflId
? () => downloadByAtflId(file.atflId!)
: undefined,
}))}
inputId={fileInputId}
isUploadingByKey={isUploading}
@@ -3,11 +3,9 @@
import { useEffect, useId, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { Paperclip } from "lucide-react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { AttachmentDownloadLink, attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
Badge,
Table,
@@ -75,6 +73,7 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
const initialFilesMappedRef = useRef(false);
const isMountedRef = useRef(true);
const atflCdByRowRef = useRef<Record<string, string>>({});
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
@@ -274,10 +273,10 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
{item.splmntDmndAtchFileGroupId ? (
<span className="inline-flex items-start gap-1">
<Paperclip className="my-1 size-5 shrink-0" />
{item.splmntDmndAtchFileNm || "첨부파일"}
</span>
<AttachmentDownloadLink
atflCd={item.splmntDmndAtchFileGroupId}
fileName={item.splmntDmndAtchFileNm}
/>
) : null}
</div>
</TableCell>
@@ -308,6 +307,9 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
files={rowFiles.map((file) => ({
fileKey: file.fileKey,
fileName: file.fileName,
onDownload: file.atflId
? () => downloadByAtflId(file.atflId!)
: undefined,
}))}
inputId={fileInputId}
isUploadingByKey={isUploading}
@@ -1,11 +1,9 @@
"use client";
import { Paperclip } from "lucide-react";
import { dayjs } from "@/shared/lib";
import type { SplmntDmndResponseExtension, SplmntDmndSaveBase } from "@/shared/model";
import { SectionCollapsible } from "@/shared/ui";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { AttachmentDownloadLink } from "@/entities/attach";
import {
Badge,
Table,
@@ -23,8 +21,6 @@ interface SupplementDemandSectionProps {
}
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
if (splmntDmndList.length === 0) {
return null;
}
@@ -70,13 +66,10 @@ function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProp
<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>
<AttachmentDownloadLink
atflCd={item.splmntDmndAtchFileGroupId}
fileName={dmndAtchFileNm || "다운로드"}
/>
) : null}
</div>
</TableCell>
@@ -89,13 +82,10 @@ function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProp
<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>
<AttachmentDownloadLink
atflCd={item.splmntAtchFileGroupId}
fileName={cmptnAtchFileNm || "다운로드"}
/>
) : null}
</div>
</TableCell>