diff --git a/.gitignore b/.gitignore index c5082bba3..7175c78d4 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ settings.json .npmrc .cursor -apps/**/tsconfig.tsbuildinfo + diff --git a/.husky/pre-push b/.husky/pre-push index 82c73e102..0ddfdb46e 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,6 @@ #!/bin/sh BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$BRANCH" != "main" ]; then + exit 0 +fi pnpm turbo run build --filter="...[origin/$BRANCH]" diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index f77bc244b..e1f6f664e 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -21,7 +21,6 @@ export default function RootLayout({ setCurrentMenuInfo(id); getMenuAuth(id.menuCd); router.push(id.urlPath); - console.log(id); }; return ( diff --git a/apps/sn/AGENTS.md b/apps/sn/AGENTS.md new file mode 100644 index 000000000..18d6c1e38 --- /dev/null +++ b/apps/sn/AGENTS.md @@ -0,0 +1,35 @@ +## Project +This repository contains a Next.js web application. + +Main technologies: +- Next.js +- TypeScript +- pnpm workspace +- turborepo + +이 저장소는 pnpm workspace + Turborepo 기반 monorepo이다. + +- 워크스페이스 범위는 `apps/*`, `apps/common/*`, `packages/*`, `packages/common/*` +- 루트 스크립트는 `turbo run build`, `turbo run dev` 를 사용한다 +- `build` 작업은 의존 패키지의 `build` 를 먼저 수행한다 (`dependsOn: ["^build"]`) +- `dev` 는 캐시하지 않는 지속 실행 작업이다 +- 변경 범위만 선택 실행할 수 있으므로, 전체 빌드보다 `--filter` 사용을 우선한다 + +## Encoding +- UTF-8 +- 한글이 깨지지 않도록 주의할 것 + +## Monorepo structure +apps/ + _template + as + cm + cs + dc + dm + dm_new + fo + sn + st + wr + diff --git a/apps/sn/app/(map-layout)/map/components/common/index.ts b/apps/sn/app/(map-layout)/map/components/common/index.ts index 2581ac0fa..d2aed5956 100644 --- a/apps/sn/app/(map-layout)/map/components/common/index.ts +++ b/apps/sn/app/(map-layout)/map/components/common/index.ts @@ -1,5 +1,5 @@ -import MsSwagePpl from "../popupComponent/MsSwagePpl"; -import sewagePump from "../popupComponent/sewagePump"; +import MsSwagePpl from "../popupComponent/MsSwagePpl"; // 하수관로 +import sewagePump from "../popupComponent/sewagePump"; // 하수펌프 import smh from "../popupComponent/smh"; import Ssc from "../popupComponent/Ssc"; import StormWaterPump from "../popupComponent/stormWaterPump"; diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index 031c69d8a..11e61b006 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -28,6 +28,10 @@ import { VerticalTable } from "@/components/ui/verticalTable"; import { useGisContext } from "@common/business_components/ui/map/Gis"; import { Button } from "@common/components/ui"; import { useEffect, useRef, useState, useMemo } from "react"; +import Style from "ol/style/Style.js"; +import Fill from "ol/style/Fill.js"; +import Stroke from "ol/style/Stroke.js"; +import CircleStyle from "ol/style/Circle.js"; import { getLayerContentComponent } from "../common"; import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table"; import InfilChart from './chart/infilChart'; @@ -38,6 +42,27 @@ import { codeMapper } from "../common/codeMapper"; import LoadingOverlay from '@/components/ui/LoadingBar'; import { searchUpperPpls } from "./smh"; +const linkedFacilityHighlightStyle = new Style({ + image: new CircleStyle({ + radius: 18, + fill: new Fill({ + color: 'rgba(255, 235, 59, 0.35)', + }), + stroke: new Stroke({ + color: 'rgba(255, 193, 7, 0.45)', + width: 2, + }), + }), + fill: new Fill({ + color: 'rgba(255, 235, 59, 0.2)', + }), + stroke: new Stroke({ + color: 'rgba(255, 193, 7, 0.35)', + width: 2.5, + }), + zIndex: 9999, +}); + const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; @@ -59,6 +84,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const [isLoading, setIsLoading] = useState(false); // 로딩 상태 const [chmbrXcnts, setChmbrXcnts] = useState(''); // 로딩 상태 const [chmbrYdnts, setChmbrYdnts] = useState(''); // 로딩 상태 + const linkedFacilityFeatureRef = useRef(null); + const linkedFacilityOriginalStyleRef = useRef(null); // 🎯 관로관리번호 추출 const pplMngNo = featureData?.values_?.fclt_mng_no || ""; @@ -77,6 +104,70 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, }; }, [featureData?.values_]); + const clearLinkedFacilityHighlight = () => { + if (linkedFacilityFeatureRef.current) { + linkedFacilityFeatureRef.current.setStyle(linkedFacilityOriginalStyleRef.current ?? undefined); + linkedFacilityFeatureRef.current.changed?.(); + } + + linkedFacilityFeatureRef.current = null; + linkedFacilityOriginalStyleRef.current = null; + }; + + // 하수처리구역에 연결된 하수처리시설 피처를 찾아 기존 아이콘 위에 노랑 하이라이트를 덧씌운다. + const highlightLinkedFacility = (facilityCode: string | null | undefined) => { + clearLinkedFacilityHighlight(); + + if (!gisMap || !facilityCode) return; + + const normalizedFacilityCode = String(facilityCode).trim(); + const layers = gisMap.getLayers().getArray(); + + for (const layer of layers) { + const source = layer?.getSource?.(); + if (!source?.getFeatures) continue; + + const features = source.getFeatures(); + const targetFeature = features.find((feature: any) => { + const featureId = feature?.getId?.(); + const isFacilityLayer = typeof featureId === 'string' && featureId.startsWith('tb_il_swage_prcs_fclt.'); + const featureFacilityCode = feature?.get?.('swtrmt_fclt_cd'); + + return isFacilityLayer && String(featureFacilityCode ?? '').trim() === normalizedFacilityCode; + }); + + if (targetFeature) { + const originalFeatureStyle = targetFeature.getStyle?.(); + const featureLayer = targetFeature.getLayer?.(); + const layerStyleFn = featureLayer?.getStyleFunction?.() ?? null; + + linkedFacilityOriginalStyleRef.current = originalFeatureStyle; + linkedFacilityFeatureRef.current = targetFeature; + targetFeature.setStyle((feature: any, resolution: number) => { + let styles: any[] = []; + + if (typeof originalFeatureStyle === 'function') { + const originalStyles = originalFeatureStyle(feature, resolution); + if (Array.isArray(originalStyles)) styles.push(...originalStyles); + else if (originalStyles) styles.push(originalStyles); + } else if (originalFeatureStyle) { + if (Array.isArray(originalFeatureStyle)) styles.push(...originalFeatureStyle); + else styles.push(originalFeatureStyle); + } else if (layerStyleFn) { + const layerStyles = layerStyleFn(feature, resolution); + if (Array.isArray(layerStyles)) styles.push(...layerStyles); + else if (layerStyles) styles.push(layerStyles); + } + + styles.push(linkedFacilityHighlightStyle); + return styles; + }); + targetFeature.changed?.(); + return; + } + } + }; + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 const getSelectedLayers = () => { let result = null; @@ -348,6 +439,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, fetchData(); }, [featureId, processedData]); + useEffect(() => { + const layerTypeNm = featureData?.values_ + ? featureData?.id_?.split('.')?.[0] + : featureData?.lyrTypeNm; + + if (layerTypeNm === 'tb_ms_swage_prcs_zone') { + const facilityCode = featureData?.values_?.swtrmt_fclt_cd ?? featureData?.swtrmtFcltCd; + highlightLinkedFacility(facilityCode); + return () => { + clearLinkedFacilityHighlight(); + }; + } + + clearLinkedFacilityHighlight(); + + return () => { + clearLinkedFacilityHighlight(); + }; + }, [featureData, gisMap]); + // 저장 시 모달 띄우기 const handleSaveClick = () => { setIsModalOpen(true); // 모달 열기 @@ -525,4 +636,4 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, ) } -export default FeatureClickEventPopup; \ No newline at end of file +export default FeatureClickEventPopup; diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index a90284dde..ad57c0a5f 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -249,7 +249,7 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg { - return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건); + return (<>조회 결과); })() } tableData={tableData} diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index d179652f3..3ce334518 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -130,7 +130,7 @@ const SearchModal: React.FC = () => { { const label = BASIC_TYPES.find((type) => type.val === basicType)?.text - return (<>{label} 조회 결과 ({tableCount.toLocaleString('ko-KR')})건); + return (<>{label} 조회 결과); })() } tableData={tableData} @@ -161,7 +161,7 @@ const SearchModal: React.FC = () => { headName={ (() => { const label = WORK_TYPES.find((type) => type.val === workType)?.text - return (<>{label} 사업 현황 ({tableCount.toLocaleString('ko-KR')})건); + return (<>{label} 사업 현황); })() } tableData={tableData} @@ -188,7 +188,7 @@ const SearchModal: React.FC = () => { headName={ (() => { const label = WORK_TYPES.find((type) => type.val === workType)?.text - return (<>{label} 결과표 ({detailTableCount.toLocaleString('ko-KR')})건); + return (<>{label} 결과표); })() } tableData={detailTableData} diff --git a/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx index f5104154f..f5fcf7d91 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx @@ -5,7 +5,7 @@ import { btnClass } from "@/app/store/common/code"; import { excelCoSttnParsing } from "@/app/store/matDmd/coSttn/excel"; import { fetchType, getFetch } from "@/components/lib/api"; import { - confirmToast, + //confirmToast, excelDownload, formatPhoneNumber, getCity, @@ -29,8 +29,10 @@ import type { DemandCompType } from "@common/business_components/ui/sdmInsertFor import { Button } from "@common/components/ui"; import moment from "moment"; import { useCallback, useEffect, useState } from "react"; -import toast, { Toaster } from "react-hot-toast"; +// import toast, { Toaster } from "react-hot-toast"; import { useTranslation } from "react-i18next"; +import { useAlertStore } from "@/src/shared/lib"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; export default function CoSttn() { const { @@ -48,7 +50,8 @@ export default function CoSttn() { getMyUUID } = useWrStore((state) => state); const { regAuthrtYn, mdfcnAuthrtYn } = auth; - + const alert = useAlertStore((state) => state.setMessage); + const { confirm } = useConfirm(); const [bdstTable, setBdstTable] = useState(); const [pageNation, setPageNation] = useState(1); const { coSttnRes, coSttnResCount } = coSttn; @@ -98,7 +101,7 @@ export default function CoSttn() { useEffect(() => { if (!coSttn.isInit) { - coSttnInit("1", "13", "0"); + coSttnInit("1", "13", "0"); // 수요처 목록 조회 } }, [coSttn.isInit, coSttnInit]); @@ -167,16 +170,21 @@ export default function CoSttn() { } }; const { t } = useTranslation('base'); - const initPassword = () => { + const initPassword = async () => { if (updateData) { const dmdInfoId = updateData.dmdInfoId; const dmdInfoSliceId = `${dmdInfoId.slice(0, 3)}-${dmdInfoId.slice(3, 5)}-${dmdInfoId.slice(5, 10)}-${dmdInfoId.slice(10)}` - const handleConfirm = () => { - coSttnPwInit(dmdInfoId); + const handleConfirm = async () => { + const message = await coSttnPwInit(dmdInfoId); + alert(message); }; - confirmToast( t('isInitPw', { dmdInfoId: dmdInfoSliceId }), handleConfirm, 'text-2xl'); + //confirmToast( t('isInitPw', { dmdInfoId: dmdInfoSliceId }), handleConfirm, 'text-2xl'); + if (await confirm(t('isInitPw', { dmdInfoId: dmdInfoSliceId }))) { + await handleConfirm(); + } } else { - toast.error("비밀번호 초기화를 원하는 행을 선택해주세요.") + alert("비밀번호 초기화를 원하는 행을 선택해주세요."); + // toast.error("비밀번호 초기화를 원하는 행을 선택해주세요.") } } @@ -238,7 +246,7 @@ export default function CoSttn() { onClick={() => { /* 저장 로직 */ if (!updateData) { - toast.error("수요처 변경을 원하는 행을 선택해주세요"); + alert("수요처 변경을 원하는 행을 선택해주세요"); } else { setIsDmdModal({ ...isDmdModal, @@ -274,8 +282,8 @@ export default function CoSttn() { headName={[]} tableData={bdstTable || []} headlist={coSttn.coSttHeadList} - pageSize={20} - total={coSttnResCount + 10 || 0} + pageSize={13} + total={coSttnResCount || 0} cellClick={ async(index: number | undefined) => { const uuid = await getMyUUID(); const updateData = coSttnRes[index as number]; @@ -332,9 +340,7 @@ export default function CoSttn() { `/reuseFc/request/${data.dmdInfoId}` ); if (dmd) { - toast.error( - `사업자번호 ${dmd.dmdInfoId}는 이미 등록되었습니다. 다시등록해주세요` - ); + alert(`사업자번호 ${dmd.dmdInfoId}는 이미 등록되었습니다. 다시등록해주세요`); return; } await regDemandCompany(data as DemandCompType); @@ -352,7 +358,7 @@ export default function CoSttn() { } > - + {/* */} ); } diff --git a/apps/wr/app/(with-layout)/matMng/coMng/ldrmntCoSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/ldrmntCoSttn/page.tsx index 8a6744833..eee7d9f73 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/ldrmntCoSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/ldrmntCoSttn/page.tsx @@ -248,7 +248,7 @@ export default function LdrmntCoSttn() {
void; coSttnSearch: (pageNo: string, pageCnt: string, con: string) => void; coSttnTxtSearch: (pageNo: string, pageCnt: string, con: string, txt: string) => void; - coSttnPwInit: (dmdInfoId: string) => void; + coSttnPwInit: (dmdInfoId: string) => Promise; }; export type coSttnType = coSttnState & coSttnActions; @@ -46,6 +45,7 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( set: any ) => { return { + // 수요처 목록 조회 coSttnInit: async (pageNo: string, pageCnt: string, con: string) => { try { @@ -126,10 +126,14 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( try { const response = await putFectch(`/reuseFc/request/chgPassword/${dmdInfoId}`,[]) if (response.status === 200) { - toast.success('비밀번호가 초기화 되었습니다.') + return "비밀번호가 초기화되었습니다."; } else { - toast.error('비밀번호 초기화에 실패하였습니다') + return "비밀번호 초기화에 실패하였습니다."; } + } catch (err) { + console.log(err); + return "비밀번호 초기화에 실패하였습니다."; + } finally { set((state: coSttnState) => { return { ...state, @@ -139,11 +143,7 @@ export const coSttnActionsExport: (set: any) => coSttnActions = ( } } }); - } catch (err) { - console.log(err); - toast.error('비밀번호 초기화에 실패하였습니다.') - } } } -} \ No newline at end of file +} diff --git a/apps/wr/src/shared/ui/alert-dialog.tsx b/apps/wr/src/shared/ui/alert-dialog.tsx index 6160d5301..51e82798c 100644 --- a/apps/wr/src/shared/ui/alert-dialog.tsx +++ b/apps/wr/src/shared/ui/alert-dialog.tsx @@ -21,10 +21,12 @@ function AlertDialog() { {message}

- - - - + +
+ + + +
diff --git a/apps/wr/src/shared/ui/confirm-dialog.tsx b/apps/wr/src/shared/ui/confirm-dialog.tsx index 6a4996bfb..7a8065a98 100644 --- a/apps/wr/src/shared/ui/confirm-dialog.tsx +++ b/apps/wr/src/shared/ui/confirm-dialog.tsx @@ -28,17 +28,19 @@ function ConfirmDialog({ {message}

- - - - - - - + +
+ + + + + + +
diff --git a/apps/wr/tsconfig.json b/apps/wr/tsconfig.json index 493584782..221de4580 100644 --- a/apps/wr/tsconfig.json +++ b/apps/wr/tsconfig.json @@ -28,13 +28,9 @@ "@/*": ["./*"], "@common/components/*": ["../../packages/common/components/src/*"], "@common/components": ["../../packages/common/components/src/index"], - "@common/business_components/*": [ - "../../packages/common/business_components/src/*" - ], - "@common/business_components": [ - "../../packages/common/business_components/src/index" - ], - "@common/network": ["../../packages/common/network"] + "@common/business_components/*": ["../../packages/common/business_components/src/*"], + "@common/business_components": ["../../packages/common/business_components/src/index"], + "@common/network": ["../../packages/common/network"], } }, "include": [ diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index d2fd81de6..e5fc933ae 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -16,6 +16,7 @@ export type ChkClass = { chk: boolean; readonly: boolean; }; + type UiTableProps = { headName: React.ReactNode | undefined; tableData: T[]; @@ -123,17 +124,20 @@ export function UiTable>({ }; const leafCols = getLeafCols(headlist); + const totalLabel = typeof total === "number" ? total.toLocaleString("ko-KR") : " "; + const hasTotal = typeof total === "number"; + return ( <> {headName ? ( headButton ? (
-

{headName}

+

{headName} 총 {totalLabel}건

<>{headButton}
) : (
-

{headName}

+

{headName} 총 {totalLabel}건

) ) : ( @@ -286,7 +290,7 @@ export function UiTable>({ )} - {total <= 0 ? ( + {!hasTotal || total <= 0 ? ( <> ) : (