diff --git a/.gitignore b/.gitignore index 871b209f0..6eedddd04 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ settings.json .npmrc .cursor .agent +CLAUDE.MD apps/**/tsconfig.tsbuildinfo packages/**/tsconfig.tsbuildinfo diff --git a/apps/as/src/entities/cnstp/model/cnstp-interface.ts b/apps/as/src/entities/cnstp/model/cnstp-interface.ts index eabd12f46..85ec716e5 100644 --- a/apps/as/src/entities/cnstp/model/cnstp-interface.ts +++ b/apps/as/src/entities/cnstp/model/cnstp-interface.ts @@ -14,6 +14,8 @@ export interface CnstpSeMngResponseDto { cnstpSeCdNm: string; upCnstpCd?: string; cdLvNo: number; + numYn: string; + dcptUnitCnt: number; useYn: string; expln?: string; sortSeq?: number; @@ -41,6 +43,8 @@ export interface CnstpSeMngSaveRequestDto { useYn?: string; expln?: string; sortSeq?: number; + numYn?: string; + dcptUnitCnt?: number; } /** PUT /{cnstpSeCd} 요청 (mode는 서비스에서 U로 설정) */ @@ -51,6 +55,8 @@ export interface CnstpSeMngPutRequestDto { cdLvNo?: number; useYn?: string; expln?: string; + numYn?: string; + dcptUnitCnt?: number; } /** 백엔드 CnstpSeMngTreeDto */ @@ -60,8 +66,11 @@ export interface CnstpSeMngTreeDto { cnstpSeCdNm: string; upCnstpCd?: string; useYn: string; + numYn: string; + dcptUnitCnt?: number; treeLevel: number; treePath: string; + sortPath: string; } /** 백엔드 BizSclTreeDto */ @@ -70,8 +79,11 @@ export interface BizSclTreeDto { cnstpSeCd: string; cnstpSeCdNm: string; useYn: string; + numYn: string; + dcptUnitCnt?: number; treeLevel: number; treePath: string; + sortPath: string; bscPlanQty?: number; bgtRfltQty?: number; tstmDesignQty?: number; diff --git a/apps/as/src/entities/dashboard/index.ts b/apps/as/src/entities/dashboard/index.ts index 19d5b2c0b..46bd4250f 100644 --- a/apps/as/src/entities/dashboard/index.ts +++ b/apps/as/src/entities/dashboard/index.ts @@ -1,6 +1,7 @@ export { dashboardService } from "./api/dashboard-service"; export { dashboardBaseKey, useGetMainDashboard } from "./api/queries"; export type { + BasinDocPrgrsResponseDto, DashboardMainResponseDto, DashboardMainSearchDto, DashboardService, diff --git a/apps/as/src/entities/dashboard/model/dashboard-interface.ts b/apps/as/src/entities/dashboard/model/dashboard-interface.ts index fb592680e..abad209c5 100644 --- a/apps/as/src/entities/dashboard/model/dashboard-interface.ts +++ b/apps/as/src/entities/dashboard/model/dashboard-interface.ts @@ -18,6 +18,18 @@ export interface DocPrgrsSttsResponseDto { totCnt: number; } +/** 백엔드 BasinDocPrgrsResponseDto */ +export interface BasinDocPrgrsResponseDto { + basinKey: string; + basinNm: string; + docSeComCd: string; + docSeNm: string; + reqCnt: number; + prgrsCnt: number; + cmptnCnt: number; + totCnt: number; +} + /** 백엔드 NotiAsUserResponseDto */ export interface NotiAsUserResponseDto { notiId: number; @@ -48,6 +60,7 @@ export interface NotiAsStatResponseDto { /** 백엔드 DashboardMainResponseDto */ export interface DashboardMainResponseDto { docPrgrsSttsList: DocPrgrsSttsResponseDto[]; + basinDocPrgrsList: BasinDocPrgrsResponseDto[]; notiList: NotiAsUserResponseDto[]; notiStats: NotiAsStatResponseDto; } diff --git a/apps/as/src/entities/tus/index.ts b/apps/as/src/entities/tus/index.ts index 5cf2e3fbf..58687d0ef 100644 --- a/apps/as/src/entities/tus/index.ts +++ b/apps/as/src/entities/tus/index.ts @@ -1 +1 @@ -export { handleTusUpload, type TusUploadOptions } from "./lib/utils"; +export { handleTusUpload, type TusErrorResponse, type TusUploadOptions } from "./lib/utils"; diff --git a/apps/as/src/entities/tus/lib/utils.ts b/apps/as/src/entities/tus/lib/utils.ts index 6bb40bfd7..96fcd5195 100644 --- a/apps/as/src/entities/tus/lib/utils.ts +++ b/apps/as/src/entities/tus/lib/utils.ts @@ -1,15 +1,29 @@ -import { Upload } from "tus-js-client"; +import { type DetailedError, Upload } from "tus-js-client"; import { ENDPOINT, PROXY_HOST } from "@/shared/config"; import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config/env"; import { getCookie } from "@/shared/lib"; +interface TusFieldError { + message: string; + field: string; + rejectedValue: string; +} + +interface TusErrorResponse { + timestamp: number[]; + message: string; + status: number; + code: string; + errors: TusFieldError[]; +} + interface TusUploadOptions { metadata?: Record; handleUploadStart?: (upload: Upload) => void; handleProgress?: (percentage: number) => void; handleSuccess?: (upload: Upload, atflCd: string, atflId: string) => void; - handleError?: (error: Error) => void; + handleError?: (error: Error, response: TusErrorResponse | null) => void; } function handleTusUpload(file: File, options?: TusUploadOptions) { @@ -32,7 +46,7 @@ function handleTusUpload(file: File, options?: TusUploadOptions) { metadata, onError(error) { console.error("TUS upload failed:", error); - options?.handleError?.(error); + options?.handleError?.(error, parseTusErrorResponse(error)); }, onProgress(bytesUploaded, bytesTotal) { const percentage = (bytesUploaded / bytesTotal) * 100; @@ -57,5 +71,22 @@ function handleTusUpload(file: File, options?: TusUploadOptions) { return upload; } +function parseTusErrorResponse(error: Error | DetailedError): TusErrorResponse | null { + if (!("originalResponse" in error)) return null; + + const body = error.originalResponse?.getBody(); + if (!body) return null; + + try { + const parsed: unknown = JSON.parse(body); + if (typeof parsed !== "object" || parsed === null) return null; + if (typeof (parsed as TusErrorResponse).message !== "string") return null; + + return parsed as TusErrorResponse; + } catch { + return null; + } +} + export { handleTusUpload }; -export type { TusUploadOptions }; +export type { TusErrorResponse, TusUploadOptions }; diff --git a/apps/as/src/pages/code-management/lib/configs/cnstp-config.ts b/apps/as/src/pages/code-management/lib/configs/cnstp-config.ts index 26bcc8000..5e11dc252 100644 --- a/apps/as/src/pages/code-management/lib/configs/cnstp-config.ts +++ b/apps/as/src/pages/code-management/lib/configs/cnstp-config.ts @@ -21,6 +21,8 @@ const buildSavePayload = createSavePayloadBuilder ({ mode: CNSTP_SE_SAVE_MODE.CREATE, @@ -31,6 +33,8 @@ const buildSavePayload = createSavePayloadBuilder ({ mode: CNSTP_SE_SAVE_MODE.UPDATE, @@ -41,6 +45,8 @@ const buildSavePayload = createSavePayloadBuilder ({ @@ -69,6 +77,8 @@ export const cnstpGridConfig: SeGridConfig< ...(patch.cnstpSeCd !== undefined ? { cnstpSeCd: patch.cnstpSeCd } : {}), ...(patch.cnstpSeCdNm !== undefined ? { cnstpSeCdNm: patch.cnstpSeCdNm ?? "" } : {}), ...(patch.expln !== undefined ? { expln: patch.expln ?? "" } : {}), + ...(patch.numYn !== undefined ? { numYn: patch.numYn } : {}), + ...(patch.dcptUnitCnt !== undefined ? { dcptUnitCnt: patch.dcptUnitCnt } : {}), ...(patch.useYn !== undefined ? { useYn: patch.useYn } : {}), ...(patch.sortSeq !== undefined ? { sortSeq: patch.sortSeq } : {}), }), @@ -77,6 +87,8 @@ export const cnstpGridConfig: SeGridConfig< cnstpSeCd: draft.cnstpSeCd, cnstpSeCdNm: draft.cnstpSeCdNm, expln: draft.expln, + numYn: draft.numYn, + dcptUnitCnt: draft.dcptUnitCnt, useYn: draft.useYn, cdLvNo: draft.cdLvNo, upCnstpCd: parentCode, diff --git a/apps/as/src/pages/code-management/model/cnstp/cnstp-se-grid-types.ts b/apps/as/src/pages/code-management/model/cnstp/cnstp-se-grid-types.ts index 9c2d37c5d..ebf1f01a0 100644 --- a/apps/as/src/pages/code-management/model/cnstp/cnstp-se-grid-types.ts +++ b/apps/as/src/pages/code-management/model/cnstp/cnstp-se-grid-types.ts @@ -13,6 +13,8 @@ export interface CnstpSeDraft { cnstpSeCd: string; cnstpSeCdNm: string; expln: string; + numYn: string; + dcptUnitCnt: number; useYn: string; sortSeq?: number; } diff --git a/apps/as/src/pages/code-management/ui/cnstp-code-tab-panel.tsx b/apps/as/src/pages/code-management/ui/cnstp-code-tab-panel.tsx index 2db182456..b915b10fb 100644 --- a/apps/as/src/pages/code-management/ui/cnstp-code-tab-panel.tsx +++ b/apps/as/src/pages/code-management/ui/cnstp-code-tab-panel.tsx @@ -118,6 +118,8 @@ function CnstpCodeTabPanel() { 코드 코드명 설명 + 숫자 여부 + 소숫점 단위 사용 여부 정렬순서 구분 diff --git a/apps/as/src/pages/code-management/ui/cnstp-se-row.tsx b/apps/as/src/pages/code-management/ui/cnstp-se-row.tsx index 299894821..85c69a3d1 100644 --- a/apps/as/src/pages/code-management/ui/cnstp-se-row.tsx +++ b/apps/as/src/pages/code-management/ui/cnstp-se-row.tsx @@ -62,6 +62,33 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns maxLength={1000} /> + + onChange({ numYn: v })} + className="flex justify-center gap-4" + > +
+ + +
+
+ + +
+
+
+ + + onChange({ dcptUnitCnt: e.target.value ? Number(e.target.value) : undefined }) + } + className="w-20" + /> + - {levelLabel} + + {levelLabel} +
diff --git a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx index dc904c38e..2093dfc48 100644 --- a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx @@ -12,8 +12,8 @@ export function DashboardPage() { return (
-
- +
+
diff --git a/apps/as/src/pages/dashboard/ui/doc-prgrs-stts-cards.tsx b/apps/as/src/pages/dashboard/ui/doc-prgrs-stts-cards.tsx index e355b0585..34180750a 100644 --- a/apps/as/src/pages/dashboard/ui/doc-prgrs-stts-cards.tsx +++ b/apps/as/src/pages/dashboard/ui/doc-prgrs-stts-cards.tsx @@ -33,7 +33,7 @@ export function DocPrgrsSttsCards({ items, isPending, isError }: DocPrgrsSttsCar const itemByCd = new Map(items?.map((item) => [item.docSeComCd, item])); return ( -
+
{DOC_PRGRS_STTS_SLOTS.map((slot) => { const item = itemByCd.get(slot.docSeComCd); return ( @@ -97,8 +97,8 @@ function DocPrgrsSttsMetric({ } const DOC_PRGRS_STTS_SLOTS = [ - { docSeComCd: "FNRS", title: "재원협의 진행상황", href: "/fnrs-cnsltn" }, { docSeComCd: "INSTL", title: "설치인가 진행상황", href: "/instl-sncn" }, + { docSeComCd: "FNRS", title: "재원협의 진행상황", href: "/fnrs-cnsltn" }, { docSeComCd: "CNFM", title: "설계자문 진행상황", href: "/design-cnstn" }, { docSeComCd: "SEWER", title: "기본계획 진행상황", href: "/sewer-bsc-plan" }, ] as const; diff --git a/apps/as/src/pages/dashboard/ui/national-as-map.tsx b/apps/as/src/pages/dashboard/ui/national-as-map.tsx index 096bbed89..82e93676c 100644 --- a/apps/as/src/pages/dashboard/ui/national-as-map.tsx +++ b/apps/as/src/pages/dashboard/ui/national-as-map.tsx @@ -2,13 +2,19 @@ import type { CSSProperties } from "react"; +import type { BasinDocPrgrsResponseDto } from "@/entities/dashboard"; + import { useNationalAsMap } from "../lib/use-national-as-map"; import styles from "../lib/national-as-map.module.css"; -export function NationalAsMap() { +interface NationalAsMapProps { + basinDocPrgrsList: BasinDocPrgrsResponseDto[] | undefined; +} + +export function NationalAsMap({ basinDocPrgrsList }: NationalAsMapProps) { const { stageRef, svgLayerRef, labelsLayerRef, popupRef, popupContent, popupGridStyle } = - useNationalAsMap(); + useNationalAsMap(basinDocPrgrsList); const popupColumnSpan = Math.max(1, popupContent.items.length); return ( diff --git a/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts index fbb6c9147..4efee2be9 100644 --- a/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts +++ b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts @@ -9,8 +9,14 @@ import { export const designCnstnRegistSchema = z.object({ designCnstnMng: designCnstnMngSaveSchema.extend({ bizNm: z.string().min(1, "사업명은 필수입니다"), - bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"), - bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"), + bizBgngYr: z + .string() + .min(1, "사업시작연도는 필수입니다") + .regex(/^\d{4}$/, "사업시작연도는 4자리 숫자여야 합니다"), + bizEndYr: z + .string() + .min(1, "사업종료연도는 필수입니다") + .regex(/^\d{4}$/, "사업종료연도는 4자리 숫자여야 합니다"), bizCn: z.string().min(1, "사업내용은 필수입니다"), taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"), dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), diff --git a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx index 85242d34b..b55dd35a2 100644 --- a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx +++ b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx @@ -106,6 +106,11 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro async function onApply(data: DesignCnstnRegistFormData) { if (isSubmitting) return; + if (data.privacyConsent !== "Y") { + alert("개인정보 수집/이용에 동의해야 신청이 가능합니다."); + return; + } + if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) { alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다."); return; diff --git a/apps/as/src/pages/design-cnstn/ui/sections/basic-info-form-section.tsx b/apps/as/src/pages/design-cnstn/ui/sections/basic-info-form-section.tsx index 9e91e9bad..92f80ac9c 100644 --- a/apps/as/src/pages/design-cnstn/ui/sections/basic-info-form-section.tsx +++ b/apps/as/src/pages/design-cnstn/ui/sections/basic-info-form-section.tsx @@ -79,9 +79,10 @@ function BasicInfoFormSection() { name="designCnstnMng.bizBgngYr" control={control} render={({ field, fieldState }) => ( - + ( - + 파일보완 -
{tableComponent}
+
{tableComponent}
{pjtcoAjmtPage?.content?.length ? ( diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx index 759b1f0df..0e7796dd2 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/basic-info-form-section.tsx @@ -95,6 +95,7 @@ function BasicInfoFormSection() { handleChange(item.code, step.planStpNo, e.target.value)} readOnly={item.readonly} diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx index c6dcdfc2f..d1855d1a1 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx @@ -6,6 +6,7 @@ import dynamic from "next/dynamic"; import { SectionCollapsible } from "@/shared/ui"; import { Field } from "@/shared/ui/field"; +import { truncateDecimals } from "@/widgets/pbadms-sprt"; import { Button, Input, @@ -28,9 +29,9 @@ const PreviousPlanLookupDialog = dynamic( ); const OTLN_ROWS = [ - { key: "sewerPlanPpltnCnt" as const, label: "계획인구" }, - { key: "sewerPrcsPpltnCnt" as const, label: "처리인구" }, - { key: "sewerPsplyRt" as const, label: "하수도보급율" }, + { key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 }, + { key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 }, + { key: "sewerPsplyRt" as const, label: "하수도보급율", step: 0.1, decimals: 1 }, ]; function OutlineFormSection() { @@ -58,8 +59,10 @@ function OutlineFormSection() { planStpNo: number, fieldKey: (typeof OTLN_ROWS)[number]["key"], value: string, + decimals?: number, ) { - const numValue = Number(value.replace(/,/g, "")) || 0; + const normalized = decimals === undefined ? value : truncateDecimals(value, decimals); + const numValue = Number(normalized.replace(/,/g, "")) || 0; const draft = [...getValues("sewerBscPlanOtlnList")]; const index = draft.findIndex((item) => item.planStpNo === planStpNo); if (index < 0) return; @@ -104,11 +107,13 @@ function OutlineFormSection() { handleChange(planStpNo, row.key, e.target.value)} + onChange={(e) => + handleChange(planStpNo, row.key, e.target.value, row.decimals) + } /> diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx index 51f59d559..62361b439 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx @@ -12,6 +12,7 @@ import { Field } from "@/shared/ui/field"; import { attachService } from "@/entities/attach"; import { useFacilityList } from "@/entities/bns-if"; import { useNowDmgMtncGnsvy } from "@/entities/dc"; +import { truncateDecimals } from "@/widgets/pbadms-sprt"; import { Button, Checkbox, @@ -57,15 +58,17 @@ interface UploadedTmplntFile { const TMPLNT_ATCH_ACCEPT = ".pdf,.png,.jpg,.jpeg"; const TMPLNT_ATCH_FORMAT_LABEL = ".pdf, .png, .jpg, .jpeg"; -const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string }[] = [ - { key: "prcsZoneArea", label: "처리구역면적(km²)" }, +const TMPLNT_DECIMAL_PLACES = 3; + +const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string; decimals?: number }[] = [ + { key: "prcsZoneArea", label: "처리구역면적(km²)", decimals: TMPLNT_DECIMAL_PLACES }, { key: "swtrmtPpltnCnt", label: "하수처리인구(명)" }, { key: "planSwqty", label: "계획하수량(㎥/일)" }, { key: "fcltCpct", label: "시설용량(㎥/일)" }, - { key: "sprsysDrtwPplLen", label: "분류식 오수(km)" }, - { key: "sprsysExlnPplLen", label: "분류식 우수(km)" }, - { key: "cbsysPplLen", label: "합류식(km)" }, - { key: "icptgPplLen", label: "차집관로(km)" }, + { key: "sprsysDrtwPplLen", label: "분류식 오수(km)", decimals: TMPLNT_DECIMAL_PLACES }, + { key: "sprsysExlnPplLen", label: "분류식 우수(km)", decimals: TMPLNT_DECIMAL_PLACES }, + { key: "cbsysPplLen", label: "합류식(km)", decimals: TMPLNT_DECIMAL_PLACES }, + { key: "icptgPplLen", label: "차집관로(km)", decimals: TMPLNT_DECIMAL_PLACES }, { key: "drgCnt", label: "배수설비(개소)" }, { key: "drtwRlyPumpCnt", label: "오수 중계 펌프(개소)" }, { key: "drtwPumpCnt", label: "오수 일반 펌프(개소)" }, @@ -76,12 +79,14 @@ interface TmplntDtlInputProps { facilityIndex: number; planStpNo: number; fieldKey: TmplntFieldKey; + decimals?: number; } -function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey }: TmplntDtlInputProps) { +function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey, decimals }: TmplntDtlInputProps) { const { control } = useFormContext(); const fieldName = `sewerBscPlanTmplntList.${facilityIndex}.sewerBscPlanTmplntDtlList.${planStpNo}.${fieldKey}` as const; + const hasDecimals = decimals !== undefined; return ( ( { - const numValue = Number(e.target.value.replace(/,/g, "")) || 0; - field.onChange(numValue); + const value = e.target.value.replace(/,/g, ""); + const normalized = hasDecimals ? truncateDecimals(value, decimals) : value; + field.onChange(Number(normalized) || 0); }} /> @@ -193,7 +200,12 @@ function TmplntFacilityMatrix({ ))} - + {changeStepHeaders.map((step) => ( @@ -201,6 +213,7 @@ function TmplntFacilityMatrix({ facilityIndex={facilityIndex} planStpNo={step.planStpNo} fieldKey={row.key} + decimals={row.decimals} /> ))} diff --git a/apps/as/src/shared/lib/use-tus.ts b/apps/as/src/shared/lib/use-tus.ts index 7a48cad6c..f07f009c0 100644 --- a/apps/as/src/shared/lib/use-tus.ts +++ b/apps/as/src/shared/lib/use-tus.ts @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { type Upload } from "tus-js-client"; -import { handleTusUpload } from "@/entities/tus"; +import { handleTusUpload, type TusErrorResponse } from "@/entities/tus"; import { useAlertStore } from "./use-alert-store"; @@ -15,7 +15,12 @@ interface UploadFileOptions { interface UseTusOptions { onSuccess?: (atflCd: string, atflId: string, file: File, fileKey: string) => void; - onError?: (error: Error, file: File, fileKey: string) => void; + onError?: ( + error: Error, + file: File, + fileKey: string, + response: TusErrorResponse | null, + ) => void; accept?: string; } @@ -27,6 +32,28 @@ interface UseTusReturn { uploads: Record; } +const ALLOWED_EXTENSIONS: readonly string[] = [ + // 문서 + ".pdf", + ".txt", + ".hwp", + ".hwpx", + ".doc", + ".docx", + ".xls", + ".xlsx", + ".ppt", + ".pptx", + // 이미지 + ".jpg", + ".jpeg", + ".png", + ".gif", + ".bmp", + // 압축 + ".zip", +]; + function getDefaultFileKey(file: File) { return `${file.name}::${file.lastModified}::${file.size}`; } @@ -39,12 +66,16 @@ function useTus(options: UseTusOptions = {}): UseTusReturn { const validateFileType = useCallback( (file: File): boolean => { - if (!options.accept) return true; - - const allowedExtensions = options.accept.split(","); const fileExtension = file.name.substring(file.name.lastIndexOf(".")).toLowerCase(); + const acceptExtensions = options.accept + ?.split(",") + .map((extension) => extension.trim().toLowerCase()); - if (!allowedExtensions.includes(fileExtension)) { + const isAllowed = + ALLOWED_EXTENSIONS.includes(fileExtension) && + (!acceptExtensions || acceptExtensions.includes(fileExtension)); + + if (!isAllowed) { alert(`허용되지 않은 파일 형식입니다. (${fileExtension})`); return false; } @@ -56,12 +87,14 @@ function useTus(options: UseTusOptions = {}): UseTusReturn { const uploadFileAsync = useCallback( (file: File, uploadOptions?: UploadFileOptions): Promise => { - if (!validateFileType(file)) { - return Promise.reject(new Error("Invalid file type")); - } - const fileKey = uploadOptions?.fileKey ?? getDefaultFileKey(file); + if (!validateFileType(file)) { + const error = new Error("Invalid file type"); + options.onError?.(error, file, fileKey, null); + return Promise.reject(error); + } + setIsUploading((prev) => ({ ...prev, [fileKey]: true })); setProgress((prev) => ({ ...prev, [fileKey]: 0 })); @@ -80,10 +113,10 @@ function useTus(options: UseTusOptions = {}): UseTusReturn { options.onSuccess?.(atflCd, atflId, file, fileKey); resolve(atflCd); }, - handleError: (error) => { + handleError: (error, response) => { setIsUploading((prev) => ({ ...prev, [fileKey]: false })); - alert("파일 업로드 중 오류가 발생했습니다."); - options.onError?.(error, file, fileKey); + alert(response?.message ?? "파일 업로드 중 오류가 발생했습니다."); + options.onError?.(error, file, fileKey, response); reject(error); }, }); diff --git a/apps/as/src/shared/ui/file-add-button.tsx b/apps/as/src/shared/ui/file-add-button.tsx index 36c41406e..922e660b8 100644 --- a/apps/as/src/shared/ui/file-add-button.tsx +++ b/apps/as/src/shared/ui/file-add-button.tsx @@ -40,7 +40,6 @@ function FileAddButton({ + {onDmndSplmnt && ( + + )}
); } diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx index db9db7c32..5a382d640 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -21,6 +21,7 @@ function HangulGiangi({ documentFileName, onSaveDocument, onDownload, + onDmndSplmnt, canSave = true, bridgeUrl = DEFAULT_BRIDGE_URL, ctrlBaseUrl = WEBHYP_CTRL_URL, @@ -206,6 +207,7 @@ function HangulGiangi({ onSave={handleSave} onDownload={handleDownload} onFullscreen={handleFullscreen} + onDmndSplmnt={onDmndSplmnt} />