From a956c9894eb5a2a048590c0d9e8847ca5a70897e Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 9 Jul 2026 11:35:50 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[st]=20-=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=A9=94=EB=89=B4=20=EC=83=88=EC=B0=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=B4=EB=8F=84=EB=A1=9D=20return=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/app/(main)/(with-base-layout)/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index 16baba982..96468f9d5 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -187,7 +187,7 @@ function WithBaseLayoutInner({ children }: { children: React.ReactNode }) { const handleMenuClick = useCallback( (item: MenuItem) => { - if (item.urlPath.includes("/cms/")) return; + // if (item.urlPath.includes("/cms/")) return; setCurrentMenuInfo(item); From 14ae432833c0efc031d740e795205265660d5106 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Fri, 10 Jul 2026 12:27:52 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[st]=20-=20fix:=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EB=8B=A8=20superAdmin=20=ED=95=84=EB=93=9C=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=8B=9C=EC=97=90=EB=A7=8C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/input/PublicSewageFacilityInp.tsx | 2 +- .../src/components/statistics/input/SewagePipeInp.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 685cea13c..120b7b8a2 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -556,7 +556,7 @@ const PublicSewageFacilityInp = () => { sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? 'superAdmin' : '', + ...(isAdminInfo ? { superAdmin: 'superAdmin' } : {}), noApplicable, saveList, }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index d49ba291a..57d3fc8e1 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1670,7 +1670,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }; @@ -1750,7 +1750,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }; @@ -1835,7 +1835,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }; @@ -1898,7 +1898,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }; @@ -1977,7 +1977,7 @@ const SewagePipeInp = () => { sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), sewerBzmnCd: values.bsnmCd ?? "", userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? "superAdmin" : "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), saveList, }; From 35a63d7c5c52a28a7b5404424b19e0928a54f05f Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 14 Jul 2026 11:54:29 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[st]=20-=20fix:=20=EC=A7=80=EB=82=9C?= =?UTF-8?q?=EB=85=84=EB=8F=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EA=B8=B0=20button=20confirm=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/button/LastYearDataButton.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/st/src/components/button/LastYearDataButton.tsx b/apps/st/src/components/button/LastYearDataButton.tsx index d9020c4ae..c0cc89f3b 100644 --- a/apps/st/src/components/button/LastYearDataButton.tsx +++ b/apps/st/src/components/button/LastYearDataButton.tsx @@ -1,12 +1,21 @@ import { Button } from "@common/components"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; interface LastYearDataButtonProps { getLastYearDataFunc: () => void; } const LastYearDataButton = ({ getLastYearDataFunc }: LastYearDataButtonProps) => { + const { confirm } = useConfirm(); + + const handleLastYearDataButton = async () => { + if (await confirm('작년 데이터를 불러오시겠습니까?')) { + getLastYearDataFunc(); + } + }; + return ( - From 18825dd0c360277b7007001899e186a432f61f92 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 15 Jul 2026 11:29:49 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[ST]=20-=20DataGrid=20setRow=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B0=84=EB=8B=A8?= =?UTF-8?q?=ED=95=9C=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/DataGrid.tsx | 29 +++++++---- .../input/PublicSewageFacilityInp.tsx | 49 ++++++++++++++++++- .../generalVerification/RegistDialog.tsx | 2 +- apps/st/src/components/type/DataGrid.type.ts | 14 +++++- .../PublicSewageFactColumn.ts | 15 +++++- .../SewerPrivateSewageTab1Column.ts | 2 +- 6 files changed, 97 insertions(+), 14 deletions(-) diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 06e0882eb..c910b104e 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -794,7 +794,24 @@ const DataGrid = ({ onCellEditedRef.current = onCellEdited; onDataCellClickRef.current = onDataCellClick; - + // rowData 변경 시 편집 중인 셀을 commit한 뒤 수동으로 setRows (RealGridReact에 rows prop을 넘기지 않음) + const rowDataMountedRef = useRef(false); + useEffect(() => { + if (!rowDataMountedRef.current) { + rowDataMountedRef.current = true; + return; + } + const gv = gridRef.current?.gridView as + | { commit?: (force?: boolean) => boolean; cancel?: () => void } + | undefined; + const dp = gridRef.current?.dataProvider as + | { setRows?: (rows: Record[]) => void } + | undefined; + if (!dp?.setRows) return; + try { gv?.commit?.(); } catch { try { gv?.cancel?.(); } catch { /* noop */ } } + dp.setRows(rowData); + }, [rowData]); + // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 useEffect(() => { if (!gridRef.current?.gridView || highlightedRowId == null) return; @@ -1673,9 +1690,11 @@ useEffect(() => { getRowCount(): number; getValue(r: number, f: string): unknown; }; + commit?: (force?: boolean) => boolean; } | undefined; if (!gv?.getDataSource) return []; + try { gv.commit?.(); } catch { /* ignore */ } const ds = gv.getDataSource(); if (!ds) return []; const cnt = ds.getRowCount(); @@ -1835,13 +1854,6 @@ useEffect(() => { if (!gridRef.current?.gridView || !showHeaderCheckbox) return; const gridView = gridRef.current.gridView as GridViewWithCheckApi; - const dataProvider = gridRef.current.dataProvider as - | { setRows?: (rows: Record[]) => void } - | undefined; - - if (dataProvider?.setRows) { - dataProvider.setRows(rowData); - } const hasCheckedColumn = rowData.some((row) => Object.prototype.hasOwnProperty.call(row, "__checked"), @@ -1989,7 +2001,6 @@ useEffect(() => { ref={gridRef} autoGenerateField={true} layout={columnLayout} - rows={rowData} onRGDataColumnsChanged={(grid) => { const dp = gridRef.current?.dataProvider as | { diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 120b7b8a2..33461faf7 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -148,6 +148,37 @@ const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from( new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)), ); +/** 위탁계약기간: `yyyymmdd~yyyymmdd` (빈 값 허용). 하이픈 형식은 정규화 */ +function normalizePrxyCtrtCn(raw: unknown): string | null { + if (raw === null || raw === undefined) return null; + const s = String(raw).trim().replace(/\s/g, ''); + if (s === '' || s === '~') return null; + const dashed = s.match(/^(\d{4})-(\d{2})-(\d{2})~(\d{4})-(\d{2})-(\d{2})$/); + if (dashed) { + return `${dashed[1]}${dashed[2]}${dashed[3]}~${dashed[4]}${dashed[5]}${dashed[6]}`; + } + return s; +} + +function isValidPrxyCtrtCn(value: string | null): boolean { + if (value === null) return true; + if (!/^\d{8}~\d{8}$/.test(value)) return false; + const start = value.slice(0, 8); + const end = value.slice(9, 17); + return start <= end; +} + +function validatePrxyCtrtCnInRows(rows: Record[]): string | null { + for (let i = 0; i < rows.length; i++) { + const normalized = normalizePrxyCtrtCn(rows[i]?.prxyCtrtCn); + if (!isValidPrxyCtrtCn(normalized)) { + const facilityNm = String(rows[i]?.swtrmtFcltNm ?? '').trim() || `${i + 1}행`; + return `위탁계약기간은 yyyymmdd~yyyymmdd 형식으로 입력해 주세요.\n(대상: ${facilityNm})`; + } + } + return null; +} + // 하수도 통계 관리 > 06. 공공하수처리시설 const PublicSewageFacilityInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -220,6 +251,11 @@ const PublicSewageFacilityInp = () => { alert('해당사항 없음을 체크해주세요.'); return; } + const prxyCtrtCnError = validatePrxyCtrtCnInRows(currentData); + if (prxyCtrtCnError) { + alert(prxyCtrtCnError); + return; + } const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); if (verificationBlocked) { alert(verificationBlocked); @@ -258,7 +294,7 @@ const PublicSewageFacilityInp = () => { prcsPhysCpct: toNumber(d.prcsPhysCpct), prcsBoilCpct: toNumber(d.prcsBoilCpct), prcsAltdCpct: toNumber(d.prcsAltdCpct), - prxyCtrtCn: d.prxyCtrtCn === '' ? null : toNullableString(d.prxyCtrtCn), + prxyCtrtCn: normalizePrxyCtrtCn(d.prxyCtrtCn), sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt), rmrk: toNullableString(d.rmrk), })); @@ -298,6 +334,12 @@ const PublicSewageFacilityInp = () => { const currentData = st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; + const prxyCtrtCnError = validatePrxyCtrtCnInRows(currentData); + if (prxyCtrtCnError) { + alert(prxyCtrtCnError); + return; + } + const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); if (!treatAsNoApplicable && submitList.length === 0) { alert('해당사항 없음을 체크해주세요.'); @@ -541,6 +583,11 @@ const PublicSewageFacilityInp = () => { const treatAsNoApplicable = noApplicable || isNotApplicable; const currentData = st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; + const prxyCtrtCnError = validatePrxyCtrtCnInRows(currentData); + if (prxyCtrtCnError) { + alert(prxyCtrtCnError); + return; + } const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); if (!treatAsNoApplicable && saveList.length === 0) { alert('해당사항 없음을 체크해주세요.'); diff --git a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx index 518f2da32..84c24aec4 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx @@ -81,7 +81,7 @@ const RegistDialog = memo( trgtStatsCd: detailData?.data.trgtStatsCd ?? '', trgtStatsNm: detailData?.data.trgtStatsNm ?? '', dcptDigtCnt: detailData?.data.dcptDigtCnt ?? 0, - dcptPrcsMthCd: detailData?.data.dcptPrcsMthCd ?? '', + dcptPrcsMthCd: detailData?.data.dcptPrcsMthCd ?? null, useYn: detailData?.data.useYn ?? 'Y', rgtrId: detailData?.data.rgtrId ?? '', regDt: detailData?.data.regDt ?? '', diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index ea4769dcb..47d0179fc 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -233,7 +233,7 @@ export type ColumnConfig = { datetimeFormat?: string; /** RealGrid 셀 편집기 */ editor?: { - type?: "dropdown" | "list" | "date"; + type?: "dropdown" | "list" | "date" | "text" | "line"; domainOnly?: boolean; textReadOnly?: boolean; commitOnSelect?: boolean; @@ -242,6 +242,18 @@ export type ColumnConfig = { datetimeFormat?: string; values?: string[]; labels?: string[]; + /** RealGrid 입력 마스크 (예: 위탁계약기간 `yyyymmdd~yyyymmdd`) */ + mask?: + | string + | { + editMask?: string; + includedFormat?: boolean; + allowEmpty?: boolean; + placeHolder?: string; + overWrite?: boolean; + showInvalidFormatMessage?: boolean; + invalidFormatMessage?: string; + }; }; footer?: | { diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts index d9943bbd6..82198b336 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -145,7 +145,20 @@ export const PublicSewageFactColumn = { // 위탁현황 { name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, - { name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' } }, + { + name: 'prxyCtrtCn', + fieldName: 'prxyCtrtCn', + header: { text: '위탁계약기간\n(yyyymmdd~yyyymmdd)' }, + editor: { + mask: { + editMask: '00000000~00000000', + includedFormat: true, + allowEmpty: true, + placeHolder: 'yyyymmdd~yyyymmdd', + overWrite: true, + }, + }, + }, // 기타 { name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false, styleName: 'rg-cell-readonly' }, diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts index c4af5d5a6..aa4893a15 100644 --- a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts @@ -4,7 +4,7 @@ export const PrivateSewageTab1ColumnConfig = { tabKey: "ST00080001", tabLabel: "처리용량별 오수처리시설", rowStartNum: 3, - fixedColumns: 3, + fixedColumns: 1, columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, From ae41483bb7a6d861eef529b99571fdd0165dcb69 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 15 Jul 2026 11:39:09 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[ST]=20-=20fix:=20=EC=A7=91=EA=B3=84=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=84=A4=EC=A0=95=20fixedColumns=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts | 2 +- .../total/sewerageFinancial/SewerageOrgCauseTab4Column.ts | 2 +- .../total/sewerageFinancial/SewerageUsageFeeTab3Column.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts index aa4893a15..7d7d6c7ba 100644 --- a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts @@ -4,7 +4,7 @@ export const PrivateSewageTab1ColumnConfig = { tabKey: "ST00080001", tabLabel: "처리용량별 오수처리시설", rowStartNum: 3, - fixedColumns: 1, + fixedColumns: 2, columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts index af0afc329..279baed23 100644 --- a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts @@ -4,7 +4,7 @@ export const SewerageOrgCauseTab4ColumnConfig = { tabKey: "ST00090004", tabLabel: "원인자부담금", rowStartNum: 3, - fixedColumns: 3, + fixedColumns: 1, columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts index 5bfe56b94..03ce956b5 100644 --- a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts @@ -4,7 +4,7 @@ export const SewerageUsageFeeTab3ColumnConfig = { tabKey: "ST00090003", tabLabel: "하수도 사용료", rowStartNum: 3, - fixedColumns: 3, + fixedColumns: 1, columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, From ddd72f71d2778f752435370875aa06921ee5983b Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 16 Jul 2026 18:24:39 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[st]=20feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=8B=9C=20X-Device-Id=20=ED=97=A4=EB=8D=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/app/api/proxy/[...url]/route.ts | 9 +++++++-- apps/st/package.json | 1 + apps/st/src/entities/auth/api/auth-service.ts | 18 +++++++++++------ apps/st/src/entities/auth/lib/device-id.ts | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 apps/st/src/entities/auth/lib/device-id.ts diff --git a/apps/st/app/api/proxy/[...url]/route.ts b/apps/st/app/api/proxy/[...url]/route.ts index c9f0d3730..261d6b259 100644 --- a/apps/st/app/api/proxy/[...url]/route.ts +++ b/apps/st/app/api/proxy/[...url]/route.ts @@ -1,3 +1,6 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + import { ACCESS_TOKEN, AUTH_USER_ID, @@ -6,8 +9,6 @@ import { SERVER_API_URL, SITE_ID, } from "@/shared/config"; -import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; async function handler( request: Request, @@ -19,6 +20,10 @@ async function handler( "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; + const deviceId = request.headers.get("X-Device-Id"); + if (deviceId) { + headers["X-Device-Id"] = deviceId; + } let body = {}; const isRefreshHandler = params.url.includes("refresh"); diff --git a/apps/st/package.json b/apps/st/package.json index 7ae6c9f53..08f5bde6b 100644 --- a/apps/st/package.json +++ b/apps/st/package.json @@ -57,6 +57,7 @@ "swr": "^2.3.7", "tailwindcss-animate": "^1.0.7", "tus-js-client": "^4.3.1", + "uuid": "^14.0.1", "xlsx": "^0.18.5", "zod": "^3.24.2", "zustand": "^5.0.3" diff --git a/apps/st/src/entities/auth/api/auth-service.ts b/apps/st/src/entities/auth/api/auth-service.ts index 48ead6a84..0c51d6dc3 100644 --- a/apps/st/src/entities/auth/api/auth-service.ts +++ b/apps/st/src/entities/auth/api/auth-service.ts @@ -11,9 +11,9 @@ import { getRemainingMs, } from "@common/auth"; +import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id"; import { clearRefreshTimer, - removeToken, setToken, setupRefreshTimer, tokenManager, @@ -24,7 +24,10 @@ import { clearTabStorePersist } from "@/store/tab-store"; async function login(payload: AuthPayload): Promise { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-Device-Id": getOrCreateDeviceId(), + }, body: JSON.stringify(payload), }); @@ -56,7 +59,12 @@ async function silentRefresh(): Promise { try { const response = await fetch( `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, - { method: "PUT" }, + { + method: "PUT", + headers: { + "X-Device-Id": getOrCreateDeviceId(), + }, + }, ); const data = await response.json(); @@ -93,9 +101,7 @@ async function silentRefresh(): Promise { export async function logout(): Promise { tokenManager.clearAuthToken(); - removeToken("site-id-key"); - removeToken("site-theme-key"); - + clearDeviceId(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); } diff --git a/apps/st/src/entities/auth/lib/device-id.ts b/apps/st/src/entities/auth/lib/device-id.ts new file mode 100644 index 000000000..a3b1aa800 --- /dev/null +++ b/apps/st/src/entities/auth/lib/device-id.ts @@ -0,0 +1,20 @@ +import { v4 as uuidv4 } from "uuid"; + +const DEVICE_ID_KEY = "x-device-id"; + +export function getOrCreateDeviceId(): string { + if (typeof window === "undefined") return ""; + + const existing = localStorage.getItem(DEVICE_ID_KEY); + if (existing) return existing; + + const id = uuidv4(); + localStorage.setItem(DEVICE_ID_KEY, id); + return id; +} + +export function clearDeviceId(): void { + if (typeof window === "undefined") return; + + localStorage.removeItem(DEVICE_ID_KEY); +}