From 2ced8d9b5c087c7b67cfdc7252dea06e5c7d64fa Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 16 Apr 2026 09:45:02 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[cs]=20chore:=20NEXT=5FPUBLIC=5FSERVER=5F?= =?UTF-8?q?API=5FURL=20->=20SERVER=5FAPI=5FURL=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/.env.example | 5 ++-- apps/cs/middleware.ts | 25 +++++++++++++------ .../editor/model/editor-base-config.ts | 6 ++--- apps/cs/src/shared/config/env.ts | 3 ++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/cs/.env.example b/apps/cs/.env.example index 9f786831b..19231af8a 100644 --- a/apps/cs/.env.example +++ b/apps/cs/.env.example @@ -1,7 +1,8 @@ # apps/cs/src/shared/config/env.ts 참고 NEXT_PUBLIC_PORT=3000 NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 +SERVER_API_URL=http://192.168.0.102:8000 NEXT_PUBLIC_SITE_ID=CS NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000 -NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 -NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 +NEXT_PUBLIC_TINY_BASE_URL= \ No newline at end of file diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index 4cc85b307..98b741325 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -1,21 +1,25 @@ import { NextRequest, NextResponse } from "next/server"; + import { - CLAIM_NAME, AUTH_USER_ID, + CLAIM_NAME, PORT, PROXY_HOST, SERVER_API_URL, SITE_ID, } from "./src/shared/config"; -const serverApiUrl = - process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; +const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000"; -const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; +const linkBackendApiHost = + process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080"; // CSP 헤더 설정 함수 (img-src만) function addSecurityHeaders(response: NextResponse): NextResponse { - response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;"); + response.headers.set( + "Content-Security-Policy", + "img-src 'self' data: https: blob:;", + ); return response; } @@ -66,7 +70,7 @@ export function middleware(req: NextRequest) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - + // /api/link 경로를 제거하고 백엔드 API 경로로 변환 const apiPath = pathname.replace("/api/link", ""); const queryParams = searchParams.toString(); @@ -94,11 +98,16 @@ export function middleware(req: NextRequest) { logMessages.push("\n--------------------------------\n"); // console.log(logMessages.join("\n")); - + const response = NextResponse.next(); return addSecurityHeaders(response); } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"], + matcher: [ + "/server/:path*", + "/attach-service/:path*", + "/api/link/:path*", + "/", + ], }; diff --git a/apps/cs/src/entities/editor/model/editor-base-config.ts b/apps/cs/src/entities/editor/model/editor-base-config.ts index ee465b675..f0684c5c8 100644 --- a/apps/cs/src/entities/editor/model/editor-base-config.ts +++ b/apps/cs/src/entities/editor/model/editor-base-config.ts @@ -1,4 +1,4 @@ -import { SERVER_API_URL } from "@/shared/config"; +import { TINY_BASE_URL } from "@/shared/config"; export const editorBaseConfig = { // 폐쇄망 환경에서는 API 키 없이 자체 호스팅 모드 사용 @@ -7,7 +7,7 @@ export const editorBaseConfig = { licenseKey: "gpl", init: { language: "ko_KR", - language_url: '/tinymce/langs/ko_KR.js', // 직접 호스팅한 경우 경로 지정 + language_url: "/tinymce/langs/ko_KR.js", // 직접 호스팅한 경우 경로 지정 height: 500, // 폐쇄망 환경을 위한 설정 base_url: "/tinymce", @@ -38,7 +38,7 @@ export const editorBaseConfig = { "undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table", "link image", ], - images_upload_base_path: SERVER_API_URL, + images_upload_base_path: TINY_BASE_URL, file_picker_types: "image", // 폐쇄망 환경을 위한 추가 설정 content_css: "/tinymce/skins/content/default/content.min.css", diff --git a/apps/cs/src/shared/config/env.ts b/apps/cs/src/shared/config/env.ts index 5e1392b4d..f352e1bd7 100644 --- a/apps/cs/src/shared/config/env.ts +++ b/apps/cs/src/shared/config/env.ts @@ -1,7 +1,7 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; -export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; +export const SERVER_API_URL = process.env.SERVER_API_URL; export const AUTH_LOGIN_REDIRECT_URL = process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login"; export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; @@ -11,4 +11,5 @@ export const ACCESS_TOKEN = export const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +export const TINY_BASE_URL = process.env.NEXT_PUBLIC_TINY_BASE_URL; export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN; From c950652f34e7792a292d0a3c651f82b1e77235e3 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Mon, 20 Apr 2026 10:39:25 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[st]=20fix:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C,=20SearchForm,=20DataGrid=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 --- .../components/button/ReviewOpinionButton.tsx | 2 +- apps/st/src/components/module/DataGrid.tsx | 7 +- apps/st/src/components/module/SearchForm.tsx | 28 +- .../statistics/input/SewagePipeInp.tsx | 267 ++++++++++------ .../input/SeweragePenetrationRateInp.tsx | 30 +- .../statistics/management/LogHistory.tsx | 297 +++++++++++++++--- .../statistics/review/ReturnReasonDialog.tsx | 99 ++++++ .../statistics/review/ReviewOpinionDialog.tsx | 168 ++++++++++ .../review/SewagePipeMaintainRvw.tsx | 89 ++++-- .../statistics/review/SewagePipeRvw.tsx | 90 ++++-- .../review/SeweragePenetrationRateRvw.tsx | 175 ++++++++++- .../statistics/review/StatsInputStatusRvw.tsx | 147 +++++++-- .../statistics/total/SewagePipeTot.tsx | 88 ++++-- .../total/SeweragePenetrationRateTot.tsx | 2 +- .../total/SewerageServiceLevelTot.tsx | 121 ++++++- apps/st/src/components/type/DataGrid.type.ts | 2 + .../st/src/components/type/SearchForm.type.ts | 5 +- .../column/management/LogHistoryColumn.ts | 16 +- .../ComplaintTotColumn.ts | 77 +++++ .../ResidentFriendlyFactTotColumn.ts | 81 +++++ .../api/statistics-management-service.ts | 14 +- .../statistics/api/statistics-service.ts | 51 ++- .../api/statistics-total-service.ts | 21 ++ .../statistics-input-review-interface.ts | 16 + .../statistics/model/statistics-interface.ts | 81 ++++- ...s-sewerage-penetration-review-interface.ts | 7 +- ...tatistics-service-level-total-interface.ts | 61 ++++ apps/st/src/shared/store/user-store.ts | 2 + 28 files changed, 1742 insertions(+), 302 deletions(-) create mode 100644 apps/st/src/components/statistics/review/ReturnReasonDialog.tsx create mode 100644 apps/st/src/components/statistics/review/ReviewOpinionDialog.tsx create mode 100644 apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts create mode 100644 apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts diff --git a/apps/st/src/components/button/ReviewOpinionButton.tsx b/apps/st/src/components/button/ReviewOpinionButton.tsx index 50df8fcb4..fbcdbd754 100644 --- a/apps/st/src/components/button/ReviewOpinionButton.tsx +++ b/apps/st/src/components/button/ReviewOpinionButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@common/components"; interface ReviewOpinionButtonProps { - reviewOpinionFunc: () => void; + reviewOpinionFunc: () => void | Promise; } const ReviewOpinionButton = ({ reviewOpinionFunc }: ReviewOpinionButtonProps) => { diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index d85693a53..9471e473a 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -311,6 +311,7 @@ function buildFieldsFromColumns( const DataGrid = ({ columnInfo, rowStartNum, + excelExportFileSuffix, isExcelImport = true, isExcelExport = true, fixedColumns, @@ -470,10 +471,14 @@ const DataGrid = ({ gridView.setColumnLayout?.(normalizedLayout); } + const exportBaseName = excelExportFileSuffix + ? `${menuName}_${excelExportFileSuffix}` + : menuName; + gridView.exportGrid({ type: "excel", target: "local", - fileName: `${menuName}.xlsx`, + fileName: `${exportBaseName}.xlsx`, footer: "hidden", allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함 done: () => { diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 44492a1dd..2284be541 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -16,28 +16,36 @@ import { } from "@common/components"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useUserStore } from "@/shared/lib"; import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type"; import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface"; +import { doList } from "../util/column/input/DoList"; -const getSidoListFetcher = async (): Promise> => { - return statisticsAPi.getSidoList(); +const getSidoListFetcher = async (key: [string, string]): Promise> => { + return statisticsAPi.getSidoList({ instSeCd: key[1] }); }; -const getSggListFetcher = async (key: [string, string]): Promise> => { - return statisticsAPi.getSggList(key[1]); +const getSggListFetcher = async (key: [string, string, string, string]): Promise> => { + const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y"; + return statisticsAPi.getSggAllList(key[1], { instSeCd: key[2], gugunSkipYn }); }; const SearchForm = (props: SearchFormProps = {}) => { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const mergedProps = { ...defaultSearchFormProps, ...props }; + const instSeCd = userInfo?.instSeCd ?? ""; + const sidoSelectText = + props.sidoSelectText && props.sidoSelectText.trim() !== "" + ? props.sidoSelectText + : "선택"; const currentYear = new Date().getFullYear(); const defaultYear = currentYear - 1; const { data: sidoList } = useSWR>( - "sido-list", + ["sido-list", instSeCd], getSidoListFetcher, { revalidateOnFocus: false, @@ -68,9 +76,9 @@ const SearchForm = (props: SearchFormProps = {}) => { isDetail: false, }); - const sggKey: [string, string] | null = + const sggKey: [string, string, string, string] | null = values.sido && values.sido !== "ALL" - ? ["sgg-list", values.sido] + ? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""] : null; const { data: sggList } = useSWR>( sggKey, @@ -152,10 +160,10 @@ const SearchForm = (props: SearchFormProps = {}) => { + + + + + 전체 + {sidoList?.data?.sidoList?.map((item) => ( + + {item.sido} + + ))} + + + +
+ + +
+
+ { className="w-[16rem]" />
-
-
- {isLoading &&
-
- + {isLoading && ( +
+
+ +
-
} + )} { ); }; -export default LogHistory; \ No newline at end of file +export default LogHistory; diff --git a/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx b/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx new file mode 100644 index 000000000..b2955bc9a --- /dev/null +++ b/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Label, + Textarea, +} from "@common/components"; + +export interface ReturnReasonDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSave?: (reason: string) => void | Promise; +} + +export function ReturnReasonDialog({ + open, + onOpenChange, + onSave, +}: ReturnReasonDialogProps) { + const [reason, setReason] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const [reasonError, setReasonError] = useState(false); + + useEffect(() => { + if (!open) return; + setReason(""); + setReasonError(false); + }, [open]); + + const handleSave = async () => { + if (reason.trim() === "") { + setReasonError(true); + return; + } + + try { + setIsSaving(true); + await onSave?.(reason); + onOpenChange(false); + } finally { + setIsSaving(false); + } + }; + + return ( + + + + 반려 사유 + +
+
+ +