Merge branch 'iipbs/st/dev'
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
<Button color="emerald" className="h-[3.2rem] w-[176px]" onClick={getLastYearDataFunc}>
|
||||
<Button color="emerald" className="h-[3.2rem] w-[176px]" onClick={handleLastYearDataButton}>
|
||||
<i className="diveicon di-search" />
|
||||
<span>지난연도 데이터 가져오기</span>
|
||||
</Button>
|
||||
|
||||
@@ -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<string, string | number>[]) => 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<string, string | number>[]) => 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
|
||||
| {
|
||||
|
||||
@@ -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, string | number>[]): 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('해당사항 없음을 체크해주세요.');
|
||||
@@ -556,7 +603,7 @@ const PublicSewageFacilityInp = () => {
|
||||
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
superAdmin: isAdminInfo ? 'superAdmin' : '',
|
||||
...(isAdminInfo ? { superAdmin: 'superAdmin' } : {}),
|
||||
noApplicable,
|
||||
saveList,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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?:
|
||||
| {
|
||||
|
||||
+14
-1
@@ -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' },
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export const PrivateSewageTab1ColumnConfig = {
|
||||
tabKey: "ST00080001",
|
||||
tabLabel: "처리용량별 오수처리시설",
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 3,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
|
||||
+1
-1
@@ -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 },
|
||||
|
||||
+1
-1
@@ -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 },
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
tokenManager.clearAuthToken();
|
||||
removeToken("site-id-key");
|
||||
removeToken("site-theme-key");
|
||||
|
||||
clearDeviceId();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user