From 54d3a35b38015bd4ff3c09bbaf884d57d92292f8 Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 4 Jun 2026 15:56:28 +0900 Subject: [PATCH 01/97] =?UTF-8?q?wr:=20store=20code=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/(map-layout)/map/mainMap/page.tsx | 7 ++++- apps/wr/app/store/common/auth/index.tsx | 2 +- apps/wr/app/store/common/code/index.ts | 31 +++++++++++++++---- apps/wr/app/store/store.tsx | 14 ++++----- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 13798ff4f..10421b48b 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -22,7 +22,7 @@ import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay"; const Map = () => { const { code, comMap, rainMapDetile, rswtMapDetile, getRuseMaplListReturn - , pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail } = useWrStore((state) => state); + , pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail, initCommonCodes } = useWrStore((state) => state); const { isFeatured } = comMap; const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const gisMaps = useRef(null); @@ -73,6 +73,11 @@ const Map = () => { }; }, []); + useEffect(() => { + if (code.isInit) return; + initCommonCodes(); + }, [code.isInit, initCommonCodes]); + // 체크된 레이어 정보를 받는 핸들러 const handleCheckedLayersChange = ( checkedIds: string[], diff --git a/apps/wr/app/store/common/auth/index.tsx b/apps/wr/app/store/common/auth/index.tsx index feca57c80..6e9396e6e 100644 --- a/apps/wr/app/store/common/auth/index.tsx +++ b/apps/wr/app/store/common/auth/index.tsx @@ -32,7 +32,7 @@ export const authStateExport: authState = { export type authActions = { getMenuAuthrts: (menuCd: number) => void; - getLaeSeCdCheck: () => Promise; + getLaeSeCdCheck: () => Promise; // 로그인 사용자 지역코드 laeSeCd를 조회 후 상태 update getMyUUID: () => Promise; }; export const authActionsExport: (set: any, get: any) => authActions = ( diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index adcff57f2..4f7896de0 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -21,12 +21,12 @@ const codeMApping = { WR002: "rswtBdstType", WR003: "rainFcltCd", WR004: "bdstGrntchClass", - WR005: "rswtPrscCdMthdCd", // 아직 디 못바꿈 - WR006: "orgwtKndCd", //작업 완ㅛ - WR007: "ruseMngMtd", // 작업완료 - WR008: "rusePrcsMthdCd", //w작업완료 - WR009: "erctUswtrPrcsMeth", //w작업완료 - WR010: "ruseFcltSpot", // inout은 yn 만 사용해야 함으로 해당 코드는 사용안함. + WR005: "rswtPrscCdMthdCd", + WR006: "orgwtKndCd", + WR007: "ruseMngMtd", + WR008: "rusePrcsMthdCd", + WR009: "erctUswtrPrcsMeth", + WR010: "ruseFcltSpot", WR011: "pplQulity", WR012: "pplLenght", WR013: "bdstFilterYn", // 사용 불가 yn만 사용해야함으로 해당코드 사용 안함 @@ -312,6 +312,7 @@ export type comCodeActions = { returnCodeText: (val: string, codeName: string) => string; returnTextCode: (text: string, codeName: string) => string; getCommonCodesByKeys: (keys: string[]) => Promise; + initCommonCodes: () => Promise; }; export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any, get:any) => { @@ -368,5 +369,23 @@ export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any const found = codeList.find((item) => item.text === text); return found ? found.val : text; }, + initCommonCodes: async () => { + if (get().code.isInit) return; + + await getSggCd(set); + await getCityCd(set); + await getRegionCd(set); + await getMiddleRegionCd(set); + await getUnitWaterShedCd(set); + await getEnvPartCd(set); + await getCommonUpCd(set); + + set((state) => ({ + code: { + ...state.code, + isInit: true, + }, + })); + }, }; }; diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index fbb3434d9..3024d1cb1 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -336,13 +336,13 @@ export const StoreZus = ( } ) => { return createStore()((set ,get) => { - getSggCd(set); - getCityCd(set); - getRegionCd(set); - getMiddleRegionCd(set); - getUnitWaterShedCd(set); - getEnvPartCd(set); - getCommonUpCd(set) + // getSggCd(set); + // getCityCd(set); + // getRegionCd(set); + // getMiddleRegionCd(set); + // getUnitWaterShedCd(set); + // getEnvPartCd(set); + // getCommonUpCd(set); return { ...initState, ...comFcltSearchAction(set), From cdfa1e56e5aa7f5f5ea135b89fc425c30ab2dd6c Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 5 Jun 2026 09:34:22 +0900 Subject: [PATCH 02/97] =?UTF-8?q?[as]=20chore:=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/acntg-code-management-page.tsx | 9 ++------- .../acntg-code-management/ui/acntg-se-row.tsx | 4 ++-- .../approval-line-management/model/columns.tsx | 4 ++-- .../ui/elapr-apln-grid.tsx | 4 ++-- .../ui/elapr-tmplt-list.tsx | 2 +- .../ui/sortable-apln-row.tsx | 16 ++++++++++++++-- .../model/cklst-artcl-columns.tsx | 2 +- .../model/cklst-mng-columns.tsx | 2 +- .../checklist-management/ui/cklst-artcl-list.tsx | 7 ++++++- .../checklist-management/ui/cklst-mng-list.tsx | 2 +- .../checklist-management/ui/cklst-rvw-grid.tsx | 3 +-- .../checklist-management/ui/cklst-rvw-row.tsx | 2 +- .../ui/cnstp-code-management-page.tsx | 9 ++------- .../cnstp-code-management/ui/cnstp-se-row.tsx | 4 ++-- .../model/esntl-atch-file-columns.tsx | 4 ++-- 15 files changed, 40 insertions(+), 34 deletions(-) diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx index 5cf1163b4..07594e1c9 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx @@ -86,15 +86,10 @@ function AcntgCodeManagementPage() {

회계코드 관리

- -
diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx index a660f86ad..a8011e6b8 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx @@ -41,7 +41,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn onChange={(e) => onChange({ acntgSeCd: e.target.value })} placeholder="코드" maxLength={10} - readOnly={!row.isNew} + disabled={!row.isNew} className="flex-1" />
@@ -90,7 +90,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn > 하위추가 - diff --git a/apps/as/src/pages/approval-line-management/model/columns.tsx b/apps/as/src/pages/approval-line-management/model/columns.tsx index aa00e9afc..a7beb668b 100644 --- a/apps/as/src/pages/approval-line-management/model/columns.tsx +++ b/apps/as/src/pages/approval-line-management/model/columns.tsx @@ -35,10 +35,10 @@ export function createTmpltColumns({ onEdit, onDelete }: CreateTmpltColumnsOptio header: "관리", cell: ({ row }) => (
e.stopPropagation()}> - -
diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx index 1dd757ff9..1371db596 100644 --- a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx @@ -151,11 +151,11 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
diff --git a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index 5539d2347..266fe0b45 100644 --- a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -111,10 +111,22 @@ function SortableAplnRow({ ) : ( <> - - diff --git a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx index c356d9e04..cc519945e 100644 --- a/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx +++ b/apps/as/src/pages/checklist-management/model/cklst-artcl-columns.tsx @@ -41,7 +41,7 @@ export function createCklstArtclColumns({ onEdit, onDelete }: CreateCklstArtclCo - diff --git a/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx b/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx index 0e0ed52a7..fd74d47ce 100644 --- a/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx +++ b/apps/as/src/pages/checklist-management/model/cklst-mng-columns.tsx @@ -54,7 +54,7 @@ export function createCklstMngColumns({ onEdit, onDelete }: CreateCklstMngColumn - diff --git a/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx b/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx index 3f4bdd3f5..480ab9508 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-artcl-list.tsx @@ -100,7 +100,12 @@ function CklstArtclList({ cklstSn }: CklstArtclListProps) {

점검항목 관리

-
diff --git a/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx b/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx index a6a3eaa0e..921295ac2 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-mng-list.tsx @@ -123,7 +123,7 @@ function CklstMngList({ selectedCklstSn, onSelectCklst }: CklstMngListProps) {

점검표 관리

-
diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx index 5ceb5add4..8eca3ce21 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx @@ -248,12 +248,11 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { > 미리보기 - - diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx index f278829fb..68b0bf8a3 100644 --- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx @@ -86,15 +86,10 @@ function CnstpCodeManagementPage() {

공종코드 관리

- -
diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx index 397621a7c..1588c12c5 100644 --- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx @@ -41,7 +41,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns onChange={(e) => onChange({ cnstpSeCd: e.target.value })} placeholder="코드" maxLength={10} - readOnly={!row.isNew} + disabled={!row.isNew} className="flex-1" />
@@ -90,7 +90,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns > 하위추가 - diff --git a/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx b/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx index ee3b04dda..ad5f32c62 100644 --- a/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx +++ b/apps/as/src/pages/esntl-atch-file-mng/model/esntl-atch-file-columns.tsx @@ -68,10 +68,10 @@ export function createEsntlAtchFileColumns({ header: "관리", cell: ({ row }) => (
e.stopPropagation()}> - -
From ec18f17c79b208f2d789d604e0398becaa44375e Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 5 Jun 2026 10:22:25 +0900 Subject: [PATCH 03/97] =?UTF-8?q?[as]=20feat:=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=9D=B8=EB=A0=A5=EA=B4=80=EB=A6=AC=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/entities/rvw-mnpw/api/queries.ts | 86 +++++++++++++++++++ .../entities/rvw-mnpw/api/rvw-mnpw-service.ts | 66 ++++++++++++++ apps/as/src/entities/rvw-mnpw/index.ts | 15 ++++ .../rvw-mnpw/model/rvw-mnpw-interface.ts | 45 ++++++++++ apps/as/src/shared/config/api.ts | 1 + 5 files changed, 213 insertions(+) create mode 100644 apps/as/src/entities/rvw-mnpw/api/queries.ts create mode 100644 apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts create mode 100644 apps/as/src/entities/rvw-mnpw/index.ts create mode 100644 apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts diff --git a/apps/as/src/entities/rvw-mnpw/api/queries.ts b/apps/as/src/entities/rvw-mnpw/api/queries.ts new file mode 100644 index 000000000..651906de0 --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/api/queries.ts @@ -0,0 +1,86 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import type { + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngUpdateRequestDto, +} from "../model/rvw-mnpw-interface"; + +import { rvwMnpwMngService } from "./rvw-mnpw-service"; + +const rvwMnpwMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW] as const; + +export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto) { + return useQuery({ + queryKey: [...rvwMnpwMngBaseKey, "findPage", params], + queryFn: () => rvwMnpwMngService.findPage(params), + }); +} + +export function useRvwMnpwMng( + atrzSeComCd: string | null | undefined, + ognzSn: number | null | undefined, + userUuid: string | null | undefined, +) { + return useQuery({ + queryKey: [...rvwMnpwMngBaseKey, "findById", atrzSeComCd, ognzSn, userUuid], + queryFn: () => rvwMnpwMngService.findById(atrzSeComCd!, ognzSn!, userUuid!), + enabled: Boolean(atrzSeComCd && ognzSn != null && userUuid), + }); +} + +export function useSaveRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: RvwMnpwMngSaveRequestDto) => rvwMnpwMngService.save(body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + }, + }); +} + +export function useUpdateRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + atrzSeComCd, + ognzSn, + userUuid, + body, + }: { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; + body: RvwMnpwMngUpdateRequestDto; + }) => rvwMnpwMngService.update(atrzSeComCd, ognzSn, userUuid, body), + onSuccess: (_data, { atrzSeComCd, ognzSn, userUuid }) => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + queryClient.invalidateQueries({ + queryKey: [...rvwMnpwMngBaseKey, "findById", atrzSeComCd, ognzSn, userUuid], + }); + }, + }); +} + +export function useDeleteRvwMnpwMng() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + atrzSeComCd, + ognzSn, + userUuid, + }: { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; + }) => rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: rvwMnpwMngBaseKey }); + }, + }); +} diff --git a/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts new file mode 100644 index 000000000..4f756787f --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/api/rvw-mnpw-service.ts @@ -0,0 +1,66 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageResponse } from "@/shared/model"; + +import type { + RvwMnpwMngResponseDto, + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngService, + RvwMnpwMngUpdateRequestDto, +} from "../model/rvw-mnpw-interface"; + +const BASE = ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW; + +function pkPath(atrzSeComCd: string, ognzSn: number, userUuid: string): string { + return `${BASE}/${atrzSeComCd}/${ognzSn}/${userUuid}`; +} + +async function findPage(params: RvwMnpwMngSearchDto): Promise> { + const response = await client.get>(BASE, { params }); + return response.data; +} + +async function findById( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, +): Promise { + const response = await client.get(pkPath(atrzSeComCd, ognzSn, userUuid)); + return response.data; +} + +async function save(body: RvwMnpwMngSaveRequestDto): Promise { + const response = await client.post(BASE, body); + return response.data; +} + +async function update( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, + body: RvwMnpwMngUpdateRequestDto, +): Promise { + const response = await client.put( + pkPath(atrzSeComCd, ognzSn, userUuid), + body, + ); + return response.data; +} + +async function deleteRvwMnpw( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, +): Promise { + const response = await client.delete(pkPath(atrzSeComCd, ognzSn, userUuid)); + return response.data; +} + +export const rvwMnpwMngService = { + findPage, + findById, + save, + update, + delete: deleteRvwMnpw, +} satisfies RvwMnpwMngService; diff --git a/apps/as/src/entities/rvw-mnpw/index.ts b/apps/as/src/entities/rvw-mnpw/index.ts new file mode 100644 index 000000000..14dd27dfc --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/index.ts @@ -0,0 +1,15 @@ +export { + useDeleteRvwMnpwMng, + useRvwMnpwMng, + useRvwMnpwMngPage, + useSaveRvwMnpwMng, + useUpdateRvwMnpwMng, +} from "./api/queries"; +export { rvwMnpwMngService } from "./api/rvw-mnpw-service"; +export type { + RvwMnpwMngResponseDto, + RvwMnpwMngSaveRequestDto, + RvwMnpwMngSearchDto, + RvwMnpwMngService, + RvwMnpwMngUpdateRequestDto, +} from "./model/rvw-mnpw-interface"; diff --git a/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts new file mode 100644 index 000000000..73aef5961 --- /dev/null +++ b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts @@ -0,0 +1,45 @@ +import type { PageableParams, PageResponse } from "@/shared/model"; + +/** 백엔드 RvwMnpwMngResponseDto */ +export interface RvwMnpwMngResponseDto { + atrzSeComCd: string; + atrzSeComCdNm?: string; + ognzSn: number; + userUuid: string; + userUuidNm?: string; + rgtr?: string; + regDt?: string; +} + +/** 백엔드 RvwMnpwMngSearchDto + Pageable */ +export interface RvwMnpwMngSearchDto extends PageableParams { + atrzSeComCd?: string; + ognzSn?: number; + userUuid?: string; +} + +/** 백엔드 RvwMnpwMngSaveRequestDto */ +export interface RvwMnpwMngSaveRequestDto { + atrzSeComCd: string; + ognzSn: number; + userUuid: string; +} + +/** 백엔드 RvwMnpwMngUpdateRequestDto */ +export interface RvwMnpwMngUpdateRequestDto { + newAtrzSeComCd: string; +} + +/** Controller public 메서드명·시그니처와 동일 */ +export interface RvwMnpwMngService { + findPage(params: RvwMnpwMngSearchDto): Promise>; + findById(atrzSeComCd: string, ognzSn: number, userUuid: string): Promise; + save(body: RvwMnpwMngSaveRequestDto): Promise; + update( + atrzSeComCd: string, + ognzSn: number, + userUuid: string, + body: RvwMnpwMngUpdateRequestDto, + ): Promise; + delete(atrzSeComCd: string, ognzSn: number, userUuid: string): Promise; +} diff --git a/apps/as/src/shared/config/api.ts b/apps/as/src/shared/config/api.ts index 84510a543..46ff4757d 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -7,6 +7,7 @@ export const ENDPOINT = { ACNTG_SE: "/as-service/api/v1/acntg-se", CNSTP_SE: "/as-service/api/v1/cnstp-se", ESNTL_ATCH_FILE: "/as-service/api/v1/esntl-atch-file", + RVW_MNPW: "/as-service/api/v1/rvw-mnpw", }, AS_INSTL_SERVICE: { INSTL_SNCN: "/as-service/api/v1/instl-sncn", From acaf2dd9f450cd0ad3cc72bce80b2b86390cbef3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 5 Jun 2026 14:32:14 +0900 Subject: [PATCH 04/97] =?UTF-8?q?[as]=20feat:=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=9D=B8=EB=A0=A5=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(with-base-layout)/rvw-mnpw-mng/page.tsx | 1 + .../docs/review-manpower-management/SPEC.md | 370 ++++++++++++++++++ .../model/reviewer-option.ts | 8 +- .../ui/aprvr-picker-dialog.tsx | 23 +- apps/as/src/pages/rvw-mnpw-mng/index.ts | 1 + .../lib/group-rvw-mnpw-by-atrz-se.ts | 18 + .../model/rvw-mnpw-block-config.ts | 16 + .../src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx | 98 +++++ .../rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx | 63 +++ .../pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx | 111 ++++++ .../rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx | 24 ++ apps/as/src/widgets/ognz-user-tree/index.ts | 4 + .../lib/filter-ognz-user-tree.ts | 0 .../model/selected-ognz-user.ts | 24 ++ .../ui/ognz-user-tree-node.tsx | 35 +- .../ognz-user-tree}/ui/ognz-user-tree.tsx | 11 +- pnpm-lock.yaml | 164 ++++---- 17 files changed, 845 insertions(+), 126 deletions(-) create mode 100644 apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx create mode 100644 apps/as/docs/review-manpower-management/SPEC.md create mode 100644 apps/as/src/pages/rvw-mnpw-mng/index.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx create mode 100644 apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx create mode 100644 apps/as/src/widgets/ognz-user-tree/index.ts rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/lib/filter-ognz-user-tree.ts (100%) create mode 100644 apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/ui/ognz-user-tree-node.tsx (75%) rename apps/as/src/{pages/approval-line-management => widgets/ognz-user-tree}/ui/ognz-user-tree.tsx (76%) diff --git a/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx b/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx new file mode 100644 index 000000000..01cf17fb5 --- /dev/null +++ b/apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx @@ -0,0 +1 @@ +export { RvwMnpwMngPage as default } from "@/pages/rvw-mnpw-mng"; diff --git a/apps/as/docs/review-manpower-management/SPEC.md b/apps/as/docs/review-manpower-management/SPEC.md new file mode 100644 index 000000000..be0211783 --- /dev/null +++ b/apps/as/docs/review-manpower-management/SPEC.md @@ -0,0 +1,370 @@ +# 화면설계서: 검토인력관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 0.2 | +| 프로세스명 | UI설계 | +| 화면명 | 검토인력관리 | +| 화면 유형 | [v] 조회 [v] 입력 [ ] 수정 [v] 삭제 [ ] 출력 | +| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 검토인력관리 | + +--- + +## 2. 화면 목적 + +조직도에서 **한 명의 사용자**를 선택한 뒤, 결재 단계(접수·검토·승인·최종승인)별 **인원 배치** 영역에 즉시 등록한다. 등록·목록 갱신은 별도 저장 버튼 없이 **[선택 인원 배치]** 클릭 시 `rvw-mnpw` API로 바로 반영한다. 좌측 조직도는 CMS 조직·사용자 트리 API를, 우측 배치 목록은 검토인력관리 API를 사용한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ 페이지 타이틀: 검토인력관리 브레드크럼: 디지털행정지원 > 사이트관리 > 검토인력관리 │ +├─────────────────────────────────┬──────────────────────────────────────────────────────────────────┤ +│ [영역 A] 조직도 │ [영역 B] 인원 배치 (결재구분별 4블록, 세로 스택) │ +│ 제목: 조직도 │ │ +│ [A-1] 검색: │ ┌─ [B-1] 접수 ───────────── count ─ [선택 인원 배치] ─┐ │ +│ [성명·부서 검색] [조회][초기화]│ │ (배치 목록 또는 빈 상태 문구) │ │ +│ 선택 안내: │ └──────────────────────────────────────────────────────┘ │ +│ "선택된 인원: {이름} │ ┌─ [B-2] 검토 ───────────── count ─ [선택 인원 배치] ──┐ │ +│ ({부서} / {직급})" │ └──────────────────────────────────────────────────────┘ │ +│ [A-2] 조직 트리 + 사용자 행 │ ┌─ [B-3] 승인 ───────────── count ─ [선택 인원 배치] ──┐ │ +│ ┌────────────────────┐ │ └──────────────────────────────────────────────────────┘ │ +│ │ 조직 트리 + 사용자 │ │ ┌─ [B-4] 최종승인 ───────── count ─ [선택 인원 배치] ──┐ │ +│ │ (단일 선택 강조) │ │ └──────────────────────────────────────────────────────┘ │ +│ └────────────────────┘ │ │ +└─────────────────────────────────┴──────────────────────────────────────────────────────────────────┘ +``` + +### 3-2. 화면 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL userOgnzService.findOgnzUserTree() + DISPLAY 조직도 트리 (영역 A) + + CALL rvwMnpwMngService.findPage({ size: 충분히 큰 값 }) // 또는 atrzSeComCd별 분할 조회 + GROUP BY atrzSeComCd + DISPLAY 인원 배치 4블록 (영역 B-1 ~ B-4) + 건수 뱃지 + + // 영역 A-1: 조직도 검색 (클라이언트 필터 — 서버 API 없음) + ON [조회] 버튼 CLICK / 검색 폼 SUBMIT DO + SET displayTree = filterOgnzUserTree(원본트리, keyword) + SET expandedOgnzSnIds = collectExpandedOgnzSnIds(displayTree) // 매칭 노드 자동 펼침 + DISPLAY 필터된 트리 (영역 A-2) + END ON + + ON [초기화] 버튼 CLICK DO + RESET keyword = "" + RESET displayTree = 원본트리 + RESET expandedOgnzSnIds = 빈 Set (또는 기본 접힘) + DISPLAY 전체 트리 (영역 A-2) + END ON + + // 영역 A-2: 사용자 단일 선택 + ON 조직도 사용자 행 CLICK DO + VALIDATE 동일 행 재클릭이 아닌 경우만 변경 + SET selectedPerson = { userUuid, userNm, ognzSn, ognzNm, jbgdSeCd, ... } + DISPLAY "선택된 인원: {userNm} ({ognzNm} / {직급명})" + END ON + + // 영역 B-k: 선택 인원 즉시 배치 + ON [B-k] [선택 인원 배치] 버튼 CLICK DO + VALIDATE selectedPerson 존재 + SET targetAtrzSeComCd = 블록 k 에 매핑된 결재구분 코드 (§4-2 표) + CALL rvwMnpwMngService.save({ + atrzSeComCd: targetAtrzSeComCd, + ognzSn: selectedPerson.ognzSn, + userUuid: selectedPerson.userUuid + }) + ON 성공 DO + CALL rvwMnpwMngService.findPage (또는 해당 블록 목록만 갱신) + DISPLAY [B-k] 배치 목록·건수 뱃지 갱신 + TOAST "배치되었습니다." + END ON + ON 실패(이미 존재) DO + TOAST (백엔드 메시지 연동) + END ON + END ON + + // 영역 B-k: 배치 인원 제거 (UI 상 삭제 UX — 스크린샷 미표기, API는 제공됨) + ON 배치 인원 [삭제] / [제거] CLICK DO + CONFIRM "해당 인원을 배치에서 제거하시겠습니까?" + CALL rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid) + CALL rvwMnpwMngService.findPage + DISPLAY [B-k] 목록·뱃지 갱신 + END ON + +END +``` + +--- + +## 4. 영역 A — 조직도 + +### 4-1. 표시·상호작용 + +| 요소 | 내용 | +| ---------------- | --------------------------------------------------------------------- | +| 섹션 제목 | 조직도 | +| 검색 폼 (A-1) | 성명·부서 키워드 입력 + **[조회]** + **[초기화]** — §4-3 | +| 선택 안내 문구 | `선택된 인원: {userNm} ({ognzNm} / {직급표시})` — 미선택 시 안내 문구 | +| 트리 (A-2) | 본부 → 팀 계층 + 팀 소속 사용자 목록 (`displayTree` 기준 표시) | +| 선택 규칙 | **사용자 1명만** 선택 가능 (라디오형 단일 선택) | +| 선택 시각 피드백 | 선택된 사용자 행 강조(배경·테두리) | +| 로딩/오류/빈값 | Skeleton / 오류 문구 / "등록된 조직/사용자가 없습니다." | +| 검색 결과 없음 | 필터 후 매칭 노드·사용자 0건. "조회된 인원이 없습니다." 문구 노출 | + +### 4-2. 조직도 데이터 매핑 + +| UI 표시 | 트리 DTO 필드 (`OgnzUserTreeResponseDto`) | 비고 | +| --------------- | ----------------------------------------- | ----------------------------------------- | +| 조직명 | `ognzNm` | 접기/펼치기 노드 | +| 사용자명 | `users[].userNm` | 클릭 시 선택 | +| 부서(안내 문구) | `ognzNm` (소속 팀 노드) | 선택 시 `selectedPerson.ognzNm` | +| 직급(안내 문구) | `users[].jbgdSeCd` 또는 `jbpsSeCd` | 공통코드 라벨 변환 | +| 저장용 조직 SN | `ognzSn` | 사용자가 속한 **리프/팀 노드**의 `ognzSn` | +| 저장용 사용자 | `users[].userUnqId` | API body `userUuid`로 전달 (§6-3) | + +### 4-3. 조직도 검색 (영역 A-1) + +결재선 관리 `AprvrPickerDialog`와 동일한 **클라이언트 사이드 필터** 패턴을 사용한다. 별도 검색 API는 없으며, `useOgnzUserTree`로 받은 **원본 트리**를 `filterOgnzUserTree`로 가공해 `displayTree`에 반영한다. + +#### 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------------- | --------- | ---- | ------ | ----------------- | ----------------------------------------- | +| 성명·부서 검색 | Text | N | (빈값) | — (클라이언트만) | `placeholder="성명·부서 검색"` | +| 조회 | Button | — | — | — | `type="submit"` — 폼 제출 시 필터 적용 | +| 초기화 | Button | — | — | — | `type="button"` — 키워드·트리·펼침 초기화 | + +#### 필터 규칙 (`filterOgnzUserTree`) + +| 대상 | 매칭 조건 | +| ---------------- | ----------------------------------------------------- | +| 조직명 | `ognzNm`에 키워드 포함 (대소문자 무시) | +| 사용자명 | `userNm`에 키워드 포함 (대소문자 무시) | +| 조직 매칭 시 | 해당 노드 **소속 사용자 전원** 유지 | +| 사용자만 매칭 시 | 매칭 사용자만 남기고 상위 조직 노드는 유지(경로 보존) | +| 하위 조직 | 재귀 필터 — 자식·손자 노드 매칭 시 상위도 표시 | +| 키워드 빈값 | 원본 트리 전체 반환 | + +#### 조회 후 트리 펼침 (`collectExpandedOgnzSnIds`) + +- **[조회]** 성공 후 필터 결과 트리의 **모든 노드** `ognzSn`을 `expandedOgnzSnIds`에 넣어 자동 펼침 +- 검색 결과에서 사용자·하위 조직을 바로 확인할 수 있도록 함 + +#### 초기화 동작 + +| 상태 | 초기화 후 값 | +| ------------------- | --------------------------------------- | +| `keyword` | `""` | +| `displayTree` | `useOgnzUserTree` 원본 `data` | +| `expandedOgnzSnIds` | `new Set()` (전체 접힘) | +| `selectedPerson` | **유지** — 검색 초기화와 선택 해제 분리 | + +#### 버튼 + +| 버튼명 | 동작 | +| ------ | --------------------------------------------------------- | +| 조회 | `filterOgnzUserTree(data, keyword)` → `displayTree` 갱신 | +| 초기화 | 키워드·표시 트리·펼침 상태 리셋 (§3-2 `ON [초기화]` 블록) | + +--- + +## 5. 영역 B — 인원 배치 + +### 5-1. 블록 공통 + +| 요소 | 내용 | +| ------------------ | ------------------------------------------------- | +| 블록 제목 | 접수 / 검토 / 승인 / 최종승인 | +| 건수 뱃지 | 해당 `atrzSeComCd`에 배치된 인원 수 | +| 액션 버튼 | **[선택 인원 배치]** — 클릭 시 즉시 `save` (§3-2) | +| 빈 상태 | "배치된 인원이 없습니다." | +| 배치 목록(있을 때) | 인원명·부서·직급 등 | + +### 5-2. 결재구분 ↔ 블록 매핑 + +| 블록 | UI 제목 | `atrzSeComCd` | 비고 | +| ---- | -------- | ------------- | -------------------------------------------------------------- | +| B-1 | 접수 | `ATRZ00` | 결재선 관리 `ATRZ_RECEIVER_CD`와 동일 | +| B-2 | 검토 | `ATRZ01` | 결재선 `ATRZ_REVIEWER_1_CD`. `ATRZ02` 별도 블록 여부 — **TBD** | +| B-3 | 승인 | `ATRZ03` | 결재선 `ATRZ_APPROVER_CD` | +| B-4 | 최종승인 | `ATRZ99` | 결재선 `ATRZ_FINAL_CD` | + +공통코드 마스터 `ATRZSE` / 화면 셀렉트용 `ATRZSECD`와 정합성은 배포 환경 코드표로 최종 확인. + +### 5-3. 배치 목록 항목 (표시) + +| 표시명 | Field (응답 DTO) | 비고 | +| -------- | ---------------- | ---------------------------- | +| 사용자명 | `userUuidNm` | `@CodeName` 미적용 시 TBD | +| 결재구분 | `atrzSeComCdNm` | 블록 제목과 중복 — 생략 가능 | +| 조직 SN | `ognzSn` | 삭제 API path에 사용 | +| 사용자 | `userUuid` | 삭제 API path에 사용 | + +### 5-4. 버튼 + +| 버튼명 | 위치 | 동작 | +| -------------- | ----------- | --------------------------------- | +| 선택 인원 배치 | 각 B-k 헤더 | `save` 후 목록·뱃지 갱신 (§3-2) | +| 제거 | 목록 항목 | `delete` 후 목록·뱃지 갱신 (§3-2) | + +### 5-5. 검증·제약 + +| 규칙 | 내용 | +| ------------- | ------------------------------------------------------------------- | +| 선택 필수 | `[선택 인원 배치]` 시 `selectedPerson` 없으면 버튼 비활성 또는 안내 | +| 단일 선택 | 조직도에서 사용자 1명만 `selectedPerson` 유지 | +| 중복 등록 | 동일 PK `(atrzSeComCd, ognzSn, userUuid)` 재등록 시 서버 오류 | +| 다건 배치 | 동일 블록에 서로 다른 사용자·조직은 **복수 행** 가능 (PK가 다름) | +| 결재구분 변경 | 본 화면 주 흐름은 `save`만 | + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +| 항목 | 내용 | +| ------------- | ------------------------------------------------------------- | +| 인증 | JWT (`Authorization` 헤더) | +| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse` | +| 프론트 엔티티 | `@/entities/user-organization`, `@/entities/rvw-mnpw` | + +### 6-2. 조직·사용자 트리 (영역 A) — `user-ognz` + +| 항목 | 내용 | +| ----------- | ------------------------------- | +| 서비스 | `cms-service` | +| Base prefix | `/cms-service/api/v1/user-ognz` | + +| 기능 | Method | URL | Request DTO | Response DTO | API | +| ---------------- | ------ | ----------- | ----------- | --------------------------- | ---------------------------------- | +| 조직·사용자 트리 | GET | `/treeuser` | — | `OgnzUserTreeResponseDto[]` | `userOgnzService.findOgnzUserTree` | + +**`OgnzUserTreeResponseDto`**: `ognzSn`, `upOgnzSn`, `ognzNm`, `users[]`, `children[]` + +**`OgnzUserTreeUserDto`**: `userUnqId`, `userNm`, `jbpsSeCd`, `jbgdSeCd` + +**React Query**: `useOgnzUserTree` + +### 6-3. 검토인력관리 (영역 B) — `rvw-mnpw` + +| 항목 | 내용 | +| ----------- | ------------------------------------- | +| 서비스 | `as-service` | +| Base prefix | `/as-service/api/v1/rvw-mnpw` | +| ENDPOINT | `ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW` | + +| 기능 | Method | URL | Request DTO / Query | Response DTO | API | +| --------------- | ------ | ------------------------------------ | -------------------------------- | ----------------------------- | ---------------------------- | +| 목록 조회 | GET | `/` | `RvwMnpwMngSearchDto` + Pageable | `Page` | `rvwMnpwMngService.findPage` | +| 상세 조회 | GET | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.findById` | +| 등록(즉시 배치) | POST | `/` | `RvwMnpwMngSaveRequestDto` | `RvwMnpwMngResponseDto` (201) | `rvwMnpwMngService.save` | +| 결재구분 변경 | PUT | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | `RvwMnpwMngUpdateRequestDto` | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.update` | +| 삭제 | DELETE | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `string` | `rvwMnpwMngService.delete` | + +**`RvwMnpwMngSearchDto`**: `atrzSeComCd?`, `ognzSn?`, `userUuid?` + `page`, `size`, `sort` + +**`RvwMnpwMngSaveRequestDto`**: `atrzSeComCd*`, `ognzSn*`, `userUuid*` + +**`RvwMnpwMngUpdateRequestDto`**: `newAtrzSeComCd*` — 본 화면 기본 흐름에서는 미사용 + +**`RvwMnpwMngResponseDto`**: `atrzSeComCd`, `atrzSeComCdNm?`, `ognzSn`, `userUuid`, `userUuidNm?`, `rgtr?`, `regDt?` + +### 6-4. 조직도 ↔ 검토인력 필드 매핑 (저장 시) + +| 조직도 (`OgnzUserTreeUserDto` + 노드) | `RvwMnpwMngSaveRequestDto` | 비고 | +| ------------------------------------- | -------------------------- | --------------------- | +| (블록 k의 `atrzSeComCd`) | `atrzSeComCd` | §5-2 표 | +| 소속 팀 `ognzSn` | `ognzSn` | | +| `userUnqId` | `userUuid` | 문자열(UUID)로 직렬화 | + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- **공통 컴포넌트**: `@common/components` (`Button`, `Badge`, `Card` 또는 섹션 레이아웃) +- **데이터**: React Query — `useOgnzUserTree`, `useRvwMnpwMngPage`, `useSaveRvwMnpwMng`, `useDeleteRvwMnpwMng` +- **상태**: `selectedPerson`(단일), `keyword`, `displayTree`, `expandedOgnzSnIds`, 블록별 배치 목록(서버 데이터 파생) +- **조직도 검색**: `filterOgnzUserTree`, `collectExpandedOgnzSnIds` (`widgets/ognz-user-tree/lib` 또는 페이지 `lib`) +- **저장 UX**: 별도 화면 저장 없음 — **[선택 인원 배치]** = `save` mutation 즉시 호출 +- **조직도 UI**: 결재선 관리 `ognz-user-tree.tsx`를 **widgets**로 승격해 공통화 + +### 7-2. 모듈·파일 배치(제안) + +| 구분 | 경로 | +| -------- | ---------------------------------------------------------------------------------- | +| 라우트 | `apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx` | +| 페이지 | `apps/as/src/pages/rvw-mnpw-mng/` | +| UI | `.../ui/rvw-mnpw-mng-page.tsx`, `rvw-mnpw-assignment-panel.tsx` 등 | +| model | `.../model/selected-ognz-user.ts`, `rvw-mnpw-block-config.ts` (`atrzSeComCd` 상수) | +| lib | `.../lib/group-rvw-mnpw-by-atrz-se.ts` (목록 → B-1~B-4 분배) | +| **위젯** | `apps/as/src/widgets/ognz-user-tree/` — 아래 이전 대상 | +| 엔티티 | `entities/user-organization/`, `entities/rvw-mnpw/` (구현 완료) | + +#### widgets/ognz-user-tree 이전 (결재선 → 공통) + +| 현재 (결재선 관리) | 이전 후 (widgets) | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| `pages/approval-line-management/ui/ognz-user-tree.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree.tsx` | +| `pages/.../ui/ognz-user-tree-node.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx` | +| `pages/.../model/reviewer-option.ts` | `widgets/ognz-user-tree/model/selected-ognz-user.ts` (이름·필드 확장: `ognzSn` 필수) | +| `pages/.../lib/filter-ognz-user-tree.ts` | `widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts` (**필수** — 검색 공통화) | +| `widgets/ognz-user-tree/index.ts` | `OgnzUserTree`, `OgnzUserTreeNode`, 타입 export | + +결재선 `AprvrPickerDialog`는 `@/widgets/ognz-user-tree` import로 교체. 검토인력관리 페이지는 **인라인 조직도**(다이얼로그 없음)로 동일 위젯 사용. + +**`SelectedOgnzUser`** — `ReviewerOption` 확장: + +```typescript +export interface SelectedOgnzUser { + userUuid: string; // OgnzUserTreeUserDto.userUnqId + userNm: string; + ognzSn: number; + ognzNm: string; + jbgdSeCd?: string; + jbpsSeCd?: string; +} +``` + +### 7-3. 영역별 UI 매핑 + +| 영역 | 주요 UI | +| ---- | -------------------------------------------------------------------- | +| A-1 | 검색 폼 (`Input` + 조회/초기화) — `AprvrPickerDialog` 90~103행 패턴 | +| A-2 | `OgnzUserTree(nodes={displayTree})` + 선택 안내 + `useOgnzUserTree` | +| B-k | 제목·Badge(count)·`선택 인원 배치`·배치 목록(또는 empty) | +| 공통 | 페이지 헤더·브레드크럼·2열 레이아웃 (`grid` / `flex`, 모바일은 스택) | + +### 7-4. 다이얼로그·라우팅 + +| 항목 | 내용 | +| ---------- | ----------------------------------- | +| 다이얼로그 | **없음** (조직도 인라인, 즉시 저장) | +| 라우팅 | 메뉴 `검토인력관리` 단일 페이지 | + +### 7-5. 데이터 흐름(구현 요약) + +- **진입**: `useOgnzUserTree` → `displayTree = data` + `useRvwMnpwMngPage({ page: 0, size: N })` → 4패널. +- **검색**: `keyword` 로컬 상태 → **[조회]** 시 `filterOgnzUserTree` + `collectExpandedOgnzSnIds` → `OgnzUserTree`에 `displayTree` 전달. +- **초기화**: `keyword`·`displayTree`·`expandedOgnzSnIds` 리셋 (`selectedPerson` 유지). +- **선택**: 트리 `onSelect` → `selectedPerson` (반드시 `ognzSn` 포함). +- **배치**: `useSaveRvwMnpwMng().mutate({ atrzSeComCd, ognzSn, userUuid })` → `onSuccess`에서 `rvw-mnpw` query invalidate. +- **삭제**: 목록 항목에서 `useDeleteRvwMnpwMng` — UI 확정 후 연결. + +### 7-6. 미확정 / 검증 필요 + +- [ ] `ATRZ02`(검토2) 별도 블록 필요 여부 +- [ ] 권한(역할별 CRUD) diff --git a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts index 569646744..3b4658acd 100644 --- a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts +++ b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts @@ -1,4 +1,4 @@ -import type { OgnzUserTreeUserDto } from "@/entities/user-organization"; +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; export interface ReviewerOption { uuid: string; @@ -6,10 +6,10 @@ export interface ReviewerOption { deptNm: string; } -export function toReviewerOption(user: OgnzUserTreeUserDto, ognzNm: string): ReviewerOption { +export function fromSelectedOgnzUser(user: SelectedOgnzUser): ReviewerOption { return { - uuid: user.userUnqId, + uuid: user.userUuid, name: user.userNm, - deptNm: ognzNm, + deptNm: user.ognzNm, }; } diff --git a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx index 8b6b7aa1d..7fbab9f17 100644 --- a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx +++ b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx @@ -3,6 +3,12 @@ import { useCallback, useEffect, useState } from "react"; import { useOgnzUserTree } from "@/entities/user-organization"; +import { + collectExpandedOgnzSnIds, + filterOgnzUserTree, + OgnzUserTree, + type SelectedOgnzUser, +} from "@/widgets/ognz-user-tree"; import { Button, Dialog, @@ -13,10 +19,7 @@ import { Input, } from "@common/components"; -import { collectExpandedOgnzSnIds, filterOgnzUserTree } from "../lib/filter-ognz-user-tree"; -import type { ReviewerOption } from "../model/reviewer-option"; - -import { OgnzUserTree } from "./ognz-user-tree"; +import { fromSelectedOgnzUser, type ReviewerOption } from "../model/reviewer-option"; interface AprvrPickerDialogProps { open: boolean; @@ -29,6 +32,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr const [displayTree, setDisplayTree] = useState>([]); const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); const [selectedUserId, setSelectedUserId] = useState(); + const [isSearchActive, setIsSearchActive] = useState(false); const { data, isPending, isError } = useOgnzUserTree({ enabled: open }); @@ -36,6 +40,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr setKeyword(""); setSelectedUserId(undefined); setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); setDisplayTree(data ?? []); }, [data]); @@ -44,6 +49,7 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr setKeyword(""); setSelectedUserId(undefined); setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); return; } @@ -66,15 +72,16 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr const filtered = filterOgnzUserTree(data ?? [], keyword); setDisplayTree(filtered); setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); + setIsSearchActive(keyword.trim().length > 0); }; const handleReset = () => { resetState(); }; - const handleSelect = (reviewer: ReviewerOption) => { - setSelectedUserId(reviewer.uuid); - onSelect(reviewer); + const handleSelect = (user: SelectedOgnzUser) => { + setSelectedUserId(user.userUuid); + onSelect(fromSelectedOgnzUser(user)); onOpenChange(false); }; @@ -108,8 +115,10 @@ function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogPr nodes={displayTree} isPending={isPending} isError={isError} + isSearchActive={isSearchActive} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton onToggle={handleToggle} onSelect={handleSelect} /> diff --git a/apps/as/src/pages/rvw-mnpw-mng/index.ts b/apps/as/src/pages/rvw-mnpw-mng/index.ts new file mode 100644 index 000000000..2b0f5d435 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/index.ts @@ -0,0 +1 @@ +export { RvwMnpwMngPage } from "./ui/rvw-mnpw-mng-page"; diff --git a/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts b/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts new file mode 100644 index 000000000..a2777d29f --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/lib/group-rvw-mnpw-by-atrz-se.ts @@ -0,0 +1,18 @@ +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; + +import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config"; + +export function groupRvwMnpwByAtrzSe( + items: RvwMnpwMngResponseDto[], + blocks: RvwMnpwBlockConfig[], +): Record { + const grouped = Object.fromEntries( + blocks.map((block) => [block.atrzSeComCd, [] as RvwMnpwMngResponseDto[]]), + ); + + for (const item of items) { + grouped[item.atrzSeComCd]?.push(item); + } + + return grouped; +} diff --git a/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts new file mode 100644 index 000000000..30dff1f37 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts @@ -0,0 +1,16 @@ +export interface RvwMnpwBlockConfig { + id: string; + title: string; + atrzSeComCd: string; +} + +/** 결재선 관리 validators.ts ATRZ 코드와 동일. ATRZSE / ATRZSECD 배포 환경 코드표 최종 확인 필요 */ +export const RVW_MNPW_BLOCKS: RvwMnpwBlockConfig[] = [ + { id: "receiver", title: "접수", atrzSeComCd: "ATRZ00" }, + // TBD: ATRZ02(검토2) 별도 블록 필요 여부 — 현재 ATRZ01만 검토 블록으로 사용 + { id: "reviewer", title: "검토", atrzSeComCd: "ATRZ01" }, + { id: "approver", title: "승인", atrzSeComCd: "ATRZ03" }, + { id: "final", title: "최종승인", atrzSeComCd: "ATRZ99" }, +]; + +export const RVW_MNPW_PAGE_SIZE = 500; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx new file mode 100644 index 000000000..172f9718b --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/ognz-panel.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; + +import { useOgnzUserTree } from "@/entities/user-organization"; +import { + collectExpandedOgnzSnIds, + filterOgnzUserTree, + OgnzUserTree, + type SelectedOgnzUser, +} from "@/widgets/ognz-user-tree"; +import { Button, Input } from "@common/components"; + +interface OgnzPanelProps { + selectedPerson: SelectedOgnzUser | null; + onSelectPerson: (person: SelectedOgnzUser) => void; +} + +function OgnzPanel({ selectedPerson, onSelectPerson }: OgnzPanelProps) { + const [keyword, setKeyword] = useState(""); + const [displayTree, setDisplayTree] = useState>([]); + const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); + const [isSearchActive, setIsSearchActive] = useState(false); + + const { data, isPending, isError } = useOgnzUserTree(); + + useEffect(() => { + setDisplayTree(data ?? []); + }, [data]); + + const handleToggle = useCallback((ognzSn: number, isExpanded: boolean) => { + setExpandedOgnzSnIds((prev) => { + const next = new Set(prev); + if (isExpanded) next.add(ognzSn); + else next.delete(ognzSn); + return next; + }); + }, []); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const filtered = filterOgnzUserTree(data ?? [], keyword); + setDisplayTree(filtered); + setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); + setIsSearchActive(keyword.trim().length > 0); + }; + + const handleReset = () => { + setKeyword(""); + setDisplayTree(data ?? []); + setExpandedOgnzSnIds(new Set()); + setIsSearchActive(false); + }; + + const handleSelect = (person: SelectedOgnzUser) => { + if (selectedPerson?.userUuid === person.userUuid) return; + onSelectPerson(person); + }; + + return ( +
+
+

조직도

+
+ +
+
+ setKeyword(e.target.value)} + /> + + +
+ +
+ +
+
+
+ ); +} + +export { OgnzPanel }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx new file mode 100644 index 000000000..f9335e63e --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useMemo } from "react"; + +import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw"; +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; +import { Skeleton } from "@common/components"; + +import { groupRvwMnpwByAtrzSe } from "../lib/group-rvw-mnpw-by-atrz-se"; +import { RVW_MNPW_BLOCKS, RVW_MNPW_PAGE_SIZE } from "../model/rvw-mnpw-block-config"; + +import { RvwMnpwBlock } from "./rvw-mnpw-block"; + +interface RvwMnpwAsmtPanelProps { + selectedPerson: SelectedOgnzUser | null; +} + +function RvwMnpwAsmtPanel({ selectedPerson }: RvwMnpwAsmtPanelProps) { + const pageQuery = useRvwMnpwMngPage({ + page: 0, + size: RVW_MNPW_PAGE_SIZE, + sort: "atrzSeComCd,asc", + }); + + const groupedItems = useMemo( + () => groupRvwMnpwByAtrzSe(pageQuery.data?.content ?? [], RVW_MNPW_BLOCKS), + [pageQuery.data?.content], + ); + + return ( +
+
+

인원 배치

+
+ +
+ {pageQuery.isPending ? ( +
+ + + + +
+ ) : pageQuery.isError ? ( +

+ 인원 배치 목록을 불러오지 못했습니다. +

+ ) : ( + RVW_MNPW_BLOCKS.map((block) => ( + + )) + )} +
+
+ ); +} + +export { RvwMnpwAsmtPanel }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx new file mode 100644 index 000000000..e03918de6 --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore } from "@/shared/lib"; +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; +import { useDeleteRvwMnpwMng, useSaveRvwMnpwMng } from "@/entities/rvw-mnpw"; +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; +import { Badge, Button } from "@common/components"; + +import type { RvwMnpwBlockConfig } from "../model/rvw-mnpw-block-config"; + +interface RvwMnpwBlockProps { + block: RvwMnpwBlockConfig; + items: RvwMnpwMngResponseDto[]; + selectedPerson: SelectedOgnzUser | null; +} + +function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + const saveMutation = useSaveRvwMnpwMng(); + const deleteMutation = useDeleteRvwMnpwMng(); + + const handleAssign = async () => { + if (!selectedPerson) return; + + try { + await saveMutation.mutateAsync({ + atrzSeComCd: block.atrzSeComCd, + ognzSn: selectedPerson.ognzSn, + userUuid: selectedPerson.userUuid, + }); + alert("배치되었습니다."); + } catch (error) { + const message = error instanceof Error ? error.message : "배치에 실패했습니다."; + alert(message); + } + }; + + const handleRemove = async (item: RvwMnpwMngResponseDto) => { + if (!(await confirm("해당 인원을 배치에서 제거하시겠습니까?"))) return; + + try { + await deleteMutation.mutateAsync({ + atrzSeComCd: item.atrzSeComCd, + ognzSn: item.ognzSn, + userUuid: item.userUuid, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "제거에 실패했습니다."; + alert(message); + } + }; + + const isAssigning = saveMutation.isPending; + const isRemoving = deleteMutation.isPending; + + return ( +
+
+
+

{block.title}

+ {items.length} +
+ +
+ + {items.length === 0 ? ( +

배치된 인원이 없습니다.

+ ) : ( +
    + {items.map((item) => ( +
  • +
    + {/* TBD: userUuidNm @CodeName 미적용 시 백엔드 enrichUserNames 결과만 표시 */} +

    + {item.userUuidNm || item.userUuid} +

    + {/* TBD: 응답 DTO에 부서·직급 필드 없음 — ognzSn만 표시 */} + {/*

    조직 SN: {item.ognzSn}

    */} +
    + +
  • + ))} +
+ )} +
+ ); +} + +export { RvwMnpwBlock }; diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx new file mode 100644 index 000000000..3e4234dcc --- /dev/null +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-mng-page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; + +import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; + +import { OgnzPanel } from "./ognz-panel"; +import { RvwMnpwAsmtPanel } from "./rvw-mnpw-asmt-panel"; + +function RvwMnpwMngPage() { + const [selectedPerson, setSelectedPerson] = useState(null); + + return ( +
+ {/* TBD: 역할별 CRUD 권한 — 메뉴·API 권한 연동 후 버튼 비활성 처리 */} +
+ + +
+
+ ); +} + +export { RvwMnpwMngPage }; diff --git a/apps/as/src/widgets/ognz-user-tree/index.ts b/apps/as/src/widgets/ognz-user-tree/index.ts new file mode 100644 index 000000000..e16df59e3 --- /dev/null +++ b/apps/as/src/widgets/ognz-user-tree/index.ts @@ -0,0 +1,4 @@ +export { collectExpandedOgnzSnIds, filterOgnzUserTree } from "./lib/filter-ognz-user-tree"; +export { type SelectedOgnzUser, toSelectedOgnzUser } from "./model/selected-ognz-user"; +export { OgnzUserTree } from "./ui/ognz-user-tree"; +export { OgnzUserTreeNode } from "./ui/ognz-user-tree-node"; diff --git a/apps/as/src/pages/approval-line-management/lib/filter-ognz-user-tree.ts b/apps/as/src/widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts similarity index 100% rename from apps/as/src/pages/approval-line-management/lib/filter-ognz-user-tree.ts rename to apps/as/src/widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts diff --git a/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts b/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts new file mode 100644 index 000000000..98e305e7e --- /dev/null +++ b/apps/as/src/widgets/ognz-user-tree/model/selected-ognz-user.ts @@ -0,0 +1,24 @@ +import type { OgnzUserTreeResponseDto, OgnzUserTreeUserDto } from "@/entities/user-organization"; + +export interface SelectedOgnzUser { + userUuid: string; + userNm: string; + ognzSn: number; + ognzNm: string; + jbgdSeCd?: string; + jbpsSeCd?: string; +} + +export function toSelectedOgnzUser( + user: OgnzUserTreeUserDto, + node: Pick, +): SelectedOgnzUser { + return { + userUuid: user.userUnqId, + userNm: user.userNm, + ognzSn: node.ognzSn, + ognzNm: node.ognzNm, + jbgdSeCd: user.jbgdSeCd || undefined, + jbpsSeCd: user.jbpsSeCd || undefined, + }; +} diff --git a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx similarity index 75% rename from apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx rename to apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx index 193cc24f2..9a8b9fd48 100644 --- a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree-node.tsx +++ b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx @@ -11,16 +11,16 @@ import { CollapsibleTrigger, } from "@common/components"; -import type { ReviewerOption } from "../model/reviewer-option"; -import { toReviewerOption } from "../model/reviewer-option"; +import { type SelectedOgnzUser, toSelectedOgnzUser } from "../model/selected-ognz-user"; interface OgnzUserTreeNodeProps { node: OgnzUserTreeResponseDto; depth?: number; expandedOgnzSnIds: Set; selectedUserId?: string; + showSelectButton?: boolean; onToggle: (ognzSn: number, open: boolean) => void; - onSelect: (reviewer: ReviewerOption) => void; + onSelect: (user: SelectedOgnzUser) => void; } function OgnzUserTreeNode({ @@ -28,6 +28,7 @@ function OgnzUserTreeNode({ depth = 0, expandedOgnzSnIds, selectedUserId, + showSelectButton = false, onToggle, onSelect, }: OgnzUserTreeNodeProps) { @@ -71,6 +72,7 @@ function OgnzUserTreeNode({ {node.users.map((user) => { const isSelected = selectedUserId === user.userUnqId; + const selectedUser = toSelectedOgnzUser(user, node); return (
onSelect(toReviewerOption(user, node.ognzNm))} + onClick={() => onSelect(selectedUser)} > {user.userNm} - + {showSelectButton ? ( + + ) : null}
); })} @@ -104,6 +108,7 @@ function OgnzUserTreeNode({ depth={depth + 1} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton={showSelectButton} onToggle={onToggle} onSelect={onSelect} /> diff --git a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx similarity index 76% rename from apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx rename to apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx index 226e878f5..968e9526c 100644 --- a/apps/as/src/pages/approval-line-management/ui/ognz-user-tree.tsx +++ b/apps/as/src/widgets/ognz-user-tree/ui/ognz-user-tree.tsx @@ -3,7 +3,7 @@ import type { OgnzUserTreeResponseDto } from "@/entities/user-organization"; import { Skeleton } from "@common/components"; -import type { ReviewerOption } from "../model/reviewer-option"; +import type { SelectedOgnzUser } from "../model/selected-ognz-user"; import { OgnzUserTreeNode } from "./ognz-user-tree-node"; @@ -11,18 +11,22 @@ interface OgnzUserTreeProps { nodes: OgnzUserTreeResponseDto[]; isPending?: boolean; isError?: boolean; + isSearchActive?: boolean; expandedOgnzSnIds: Set; selectedUserId?: string; + showSelectButton?: boolean; onToggle: (ognzSn: number, open: boolean) => void; - onSelect: (reviewer: ReviewerOption) => void; + onSelect: (user: SelectedOgnzUser) => void; } function OgnzUserTree({ nodes, isPending, isError, + isSearchActive = false, expandedOgnzSnIds, selectedUserId, + showSelectButton = false, onToggle, onSelect, }: OgnzUserTreeProps) { @@ -47,7 +51,7 @@ function OgnzUserTree({ if (nodes.length === 0) { return (

- 등록된 조직/사용자가 없습니다. + {isSearchActive ? "조회된 인원이 없습니다." : "등록된 조직/사용자가 없습니다."}

); } @@ -60,6 +64,7 @@ function OgnzUserTree({ node={node} expandedOgnzSnIds={expandedOgnzSnIds} selectedUserId={selectedUserId} + showSelectButton={showSelectButton} onToggle={onToggle} onSelect={onSelect} /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8919f1d1..b20d42ab4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,8 +92,8 @@ importers: specifier: ^1.11.13 version: 1.11.20 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -103,10 +103,10 @@ importers: devDependencies: eslint: specifier: ^9.0.0 - version: 9.39.4(jiti@2.6.1) + version: 9.39.4(jiti@1.21.7) eslint-config-next: specifier: 15.3.8 - version: 15.3.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 15.3.8(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8.4.49 version: 8.5.8 @@ -180,8 +180,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -200,13 +200,13 @@ importers: version: 3.0.6 eslint: specifier: ^9.0.0 - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 15.3.8 - version: 15.3.8(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + version: 15.3.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.2.0(eslint@9.39.4(jiti@1.21.7)) + version: 5.2.0(eslint@9.39.4(jiti@2.6.1)) postcss: specifier: ^8.4.49 version: 8.5.8 @@ -313,8 +313,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -443,8 +443,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -594,8 +594,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -712,8 +712,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -812,8 +812,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-hook-form: specifier: ^7.54.2 version: 7.72.0(react@19.2.4) @@ -897,8 +897,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -1003,8 +1003,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -1193,8 +1193,8 @@ importers: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-qrcode: specifier: ^2.5.1 version: 2.5.1(react@19.2.4) @@ -1341,8 +1341,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 next: - specifier: 15.3.8 - version: 15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 15.5.18 + version: 15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ol: specifier: ^7.3.0 version: 7.5.2 @@ -3397,10 +3397,10 @@ packages: integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==, } - "@next/env@15.3.8": + "@next/env@15.5.18": resolution: { - integrity: sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==, + integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==, } "@next/eslint-plugin-next@15.3.8": @@ -3409,73 +3409,73 @@ packages: integrity: sha512-CKpvDZhNXPUjUHmjHddcsqjwj46oQblFms2hr2uA79C0kVTiSXQEQxkawUsDNSofHhTI31IbSeBdVyuk/U1nKw==, } - "@next/swc-darwin-arm64@15.3.5": + "@next/swc-darwin-arm64@15.5.18": resolution: { - integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==, + integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==, } engines: { node: ">= 10" } cpu: [arm64] os: [darwin] - "@next/swc-darwin-x64@15.3.5": + "@next/swc-darwin-x64@15.5.18": resolution: { - integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==, + integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==, } engines: { node: ">= 10" } cpu: [x64] os: [darwin] - "@next/swc-linux-arm64-gnu@15.3.5": + "@next/swc-linux-arm64-gnu@15.5.18": resolution: { - integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==, + integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-arm64-musl@15.3.5": + "@next/swc-linux-arm64-musl@15.5.18": resolution: { - integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==, + integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==, } engines: { node: ">= 10" } cpu: [arm64] os: [linux] - "@next/swc-linux-x64-gnu@15.3.5": + "@next/swc-linux-x64-gnu@15.5.18": resolution: { - integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==, + integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-linux-x64-musl@15.3.5": + "@next/swc-linux-x64-musl@15.5.18": resolution: { - integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==, + integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==, } engines: { node: ">= 10" } cpu: [x64] os: [linux] - "@next/swc-win32-arm64-msvc@15.3.5": + "@next/swc-win32-arm64-msvc@15.5.18": resolution: { - integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==, + integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==, } engines: { node: ">= 10" } cpu: [arm64] os: [win32] - "@next/swc-win32-x64-msvc@15.3.5": + "@next/swc-win32-x64-msvc@15.5.18": resolution: { - integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==, + integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==, } engines: { node: ">= 10" } cpu: [x64] @@ -4776,12 +4776,6 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - "@swc/counter@0.1.3": - resolution: - { - integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==, - } - "@swc/helpers@0.5.15": resolution: { @@ -6021,13 +6015,6 @@ packages: peerDependencies: esbuild: ">=0.18" - busboy@1.6.0: - resolution: - { - integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==, - } - engines: { node: ">=10.16.0" } - cac@6.7.14: resolution: { @@ -8743,16 +8730,16 @@ packages: react: ^19.2.1 react-dom: ^19.2.1 - next@15.3.8: + next@15.5.18: resolution: { - integrity: sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==, + integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==, } engines: { node: ^18.18.0 || ^19.8.0 || >= 20.0.0 } hasBin: true peerDependencies: "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 + "@playwright/test": ^1.51.1 babel-plugin-react-compiler: "*" react: ^19.2.1 react-dom: ^19.2.1 @@ -10137,13 +10124,6 @@ packages: prettier: optional: true - streamsearch@1.1.0: - resolution: - { - integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==, - } - engines: { node: ">=10.0.0" } - string-argv@0.3.2: resolution: { @@ -11946,34 +11926,34 @@ snapshots: "@tybys/wasm-util": 0.10.1 optional: true - "@next/env@15.3.8": {} + "@next/env@15.5.18": {} "@next/eslint-plugin-next@15.3.8": dependencies: fast-glob: 3.3.1 - "@next/swc-darwin-arm64@15.3.5": + "@next/swc-darwin-arm64@15.5.18": optional: true - "@next/swc-darwin-x64@15.3.5": + "@next/swc-darwin-x64@15.5.18": optional: true - "@next/swc-linux-arm64-gnu@15.3.5": + "@next/swc-linux-arm64-gnu@15.5.18": optional: true - "@next/swc-linux-arm64-musl@15.3.5": + "@next/swc-linux-arm64-musl@15.5.18": optional: true - "@next/swc-linux-x64-gnu@15.3.5": + "@next/swc-linux-x64-gnu@15.5.18": optional: true - "@next/swc-linux-x64-musl@15.3.5": + "@next/swc-linux-x64-musl@15.5.18": optional: true - "@next/swc-win32-arm64-msvc@15.3.5": + "@next/swc-win32-arm64-msvc@15.5.18": optional: true - "@next/swc-win32-x64-msvc@15.3.5": + "@next/swc-win32-x64-msvc@15.5.18": optional: true "@nodelib/fs.scandir@2.1.5": @@ -12904,8 +12884,6 @@ snapshots: dependencies: storybook: 8.6.18(prettier@3.8.1) - "@swc/counter@0.1.3": {} - "@swc/helpers@0.5.15": dependencies: tslib: 2.8.1 @@ -13821,10 +13799,6 @@ snapshots: esbuild: 0.24.2 load-tsconfig: 0.2.5 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -15642,26 +15616,24 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - next@15.3.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@15.5.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - "@next/env": 15.3.8 - "@swc/counter": 0.1.3 + "@next/env": 15.5.18 "@swc/helpers": 0.5.15 - busboy: 1.6.0 caniuse-lite: 1.0.30001781 postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - "@next/swc-darwin-arm64": 15.3.5 - "@next/swc-darwin-x64": 15.3.5 - "@next/swc-linux-arm64-gnu": 15.3.5 - "@next/swc-linux-arm64-musl": 15.3.5 - "@next/swc-linux-x64-gnu": 15.3.5 - "@next/swc-linux-x64-musl": 15.3.5 - "@next/swc-win32-arm64-msvc": 15.3.5 - "@next/swc-win32-x64-msvc": 15.3.5 + "@next/swc-darwin-arm64": 15.5.18 + "@next/swc-darwin-x64": 15.5.18 + "@next/swc-linux-arm64-gnu": 15.5.18 + "@next/swc-linux-arm64-musl": 15.5.18 + "@next/swc-linux-x64-gnu": 15.5.18 + "@next/swc-linux-x64-musl": 15.5.18 + "@next/swc-win32-arm64-msvc": 15.5.18 + "@next/swc-win32-x64-msvc": 15.5.18 sharp: 0.34.5 transitivePeerDependencies: - "@babel/core" @@ -16547,8 +16519,6 @@ snapshots: - supports-color - utf-8-validate - streamsearch@1.1.0: {} - string-argv@0.3.2: {} string-width@4.2.3: From 3fee2a781fdbc5aa072d83ec533754858c767692 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 5 Jun 2026 16:15:46 +0900 Subject: [PATCH 05/97] =?UTF-8?q?wr:=20=ED=95=98=EC=88=98=EC=9E=AC?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=EC=8B=9C=EC=84=A4=EB=93=B1=EB=A1=9D=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 --- .../ruseFcltMng/fcltReg/pbnstReg/page.tsx | 29 ++++----- apps/wr/app/api/proxy/[...url]/route.ts | 2 +- .../ruseFcltMng/fcltReg/pbnstReg/index.tsx | 63 +++++++++++++------ .../ruseFcltMng/fcltSttn/pbnstSttn/index.tsx | 3 +- apps/wr/app/store/store.tsx | 2 +- apps/wr/components/lib/func/common.tsx | 6 +- 6 files changed, 65 insertions(+), 40 deletions(-) diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx index 75d718411..51b5c255a 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx @@ -31,7 +31,6 @@ import { btnClass } from "@/app/store/common/code"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components"; import { useCallback, useEffect, useRef, useState } from "react"; import ListDiv from "@common/business_components/ui/list_div"; -import { tableData } from "@/components/lib/func/common"; import type { customPbnst, fcBizType, @@ -56,7 +55,6 @@ import { useAlertStore } from "@/src/shared/lib"; export default function PublicReuseFaciltyInsert() { const { auth, - code, pbnst, pubHasuInit, pbnstInsert, @@ -112,11 +110,11 @@ export default function PublicReuseFaciltyInsert() { useInitRef.current = true; }, []); - useEffect(() => { - if (useInitRef.current == true && laeSeCd !== undefined) { - pubHasuInit("1", laeSeCd); //page: string, sggOrCity: string - } - }, [useInitRef.current, laeSeCd]); + // useEffect(() => { + // if (useInitRef.current == true && laeSeCd !== undefined) { + // pubHasuInit(1, laeSeCd); + // } + // }, [useInitRef.current, laeSeCd]); const onSubmit = async (valuse: { items: PbnstRegArrrayType }) => { const data = writeRgtrAndMdfr(valuse.items); @@ -156,7 +154,7 @@ export default function PublicReuseFaciltyInsert() { message += "하수재이용 시설 사업 "; } alert(message + "오류 입니다 "); - pubHasuInit("1", laeSeCd); + pubHasuInit(1, laeSeCd); } }; if (await confirm(t("isUpdatetMsg"))) { @@ -233,10 +231,12 @@ export default function PublicReuseFaciltyInsert() { }, [items]); useEffect(() => { - if (code.sggCd.length && pbnstHasuRes.length > 0) { - setbdstTable(tableData(pbnstHasuRes, { code })); + if (pbnstHasuRes.length > 0) { + setbdstTable(pbnstHasuRes); + } else { + setbdstTable([]); } - }, [code, pbnstHasuRes]); + }, [pbnstHasuRes]); const cellClick = (val: number | undefined) => { if (val === undefined) return; @@ -288,7 +288,8 @@ export default function PublicReuseFaciltyInsert() { }, [pbnstRes, append, selectRow]); useEffect(() => { - pubHasuInit(String(pageNation), laeSeCd); // 해당 지자체의 하수처리시설 조회 + if (laeSeCd === undefined) return; + pubHasuInit(pageNation, laeSeCd); // 해당 지자체의 하수처리시설 조회 isResetPage(false); setSelectRow(undefined); setSelectedRowIndex(-1); @@ -316,9 +317,9 @@ export default function PublicReuseFaciltyInsert() { // size={"sm"} onClick={() => { if (searchTxt && searchTxt?.length > 0) { - pubHasuSearch(String(pageNation), laeSeCd, searchTxt); + pubHasuSearch(pageNation, laeSeCd, searchTxt); } else { - pubHasuInit(String(pageNation), laeSeCd); + pubHasuInit(pageNation, laeSeCd); } isResetPage(true); }} diff --git a/apps/wr/app/api/proxy/[...url]/route.ts b/apps/wr/app/api/proxy/[...url]/route.ts index e847e8ad2..d779aec83 100644 --- a/apps/wr/app/api/proxy/[...url]/route.ts +++ b/apps/wr/app/api/proxy/[...url]/route.ts @@ -75,7 +75,7 @@ async function handler( if (refreshToken) { response.cookies.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, - sameSite: "strict", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정 + sameSite: "lax", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정 domain: getCookieDomain(), path: "/", }); diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx index 6340f1082..887f437a7 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx @@ -5,6 +5,7 @@ import { getFetch, postFectch, putFectch, + getFetchWithCount } from "@/components/lib/api"; import { DEFAULT_LIST_SIZE, SITE_ID } from "@/src/shared/config"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; @@ -61,10 +62,11 @@ export type pubFacState = { dmdComList: any[]; apiMessage: string; pbnstHasuResCount: number; + fclts: any[] | null; }; }; type pubFacActions = { - pubHasuInit: (page: string, sggOrCity: string) => void; + pubHasuInit: (page: number, sggOrCity: string) => void; pbnstRegClick: (pubHasuRes: pubHasuType) => void; //pbnstReg: (txt: string | undefined) => void; pbnstInsert(pbnstRes: customPbnst): Promise; @@ -73,7 +75,7 @@ type pubFacActions = { fileDownLoadName: (atflCd: string, fileName: string) => void; pbnstRuseFcltUpdate: (ruseRes: customPbnst[]) => void; pbnstRuseBizUpdate: (ruseRes: fcBizType[]) => void; - pubHasuSearch: (page: string, sggOrCity: string, searchTxt: string) => void; + pubHasuSearch: (page: number, sggOrCity: string, searchTxt: string) => void; getFcOrgwts: (fcltPkList: string[], size: number) => void; }; export type pubFacType = pubFacState & pubFacActions; @@ -98,38 +100,59 @@ export const pubFacStateExport: pubFacState = { dmdComList: [], apiMessage: "", pbnstHasuResCount: 1, + fclts: null, }, }; // 재사용시설에 대한 정보 조회 -export const checkWrRegList = async () => { +export const checkWrRegList = async (get: any, set: any) => { + const { pbnst } = get(); + + if (Array.isArray(pbnst.fclts)) { + return pbnst.fclts; + } + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); - return wrRegList; + const fclts = Array.isArray(wrRegList) ? wrRegList : []; + + set((state: pubFacState) => ({ + ...state, + pbnst: { + ...state.pbnst, + fclts, + }, + })); + + return fclts; }; // 하수처리시설 정보 조회 const hasuListFetch = async ( - page: string, + get: any, + set: any, + page: number = 1, sggOrCity: string, // 해당 지자체의 하수처리시설 조회 searchTxt?: string ) => { - const checkWrRegListRes = await checkWrRegList(); // 재사용시설에 대한 정보 조회 + const wrData = await checkWrRegList(get, set); // 재사용시설에 대한 정보 조회 + const wrBscFctCdSet = new Set( + wrData + .map((wrItem: any) => (typeof wrItem === "string" ? wrItem.trim() : "")) + ); - let url = `/prcsFclt/ruseInfo/${page ? page : 1}/${DEFAULT_LIST_SIZE}/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`; - - const data = checkWrRegListRes.filter((item: string) => item !== ""); - if (searchTxt) { - url = url + `/${searchTxt}`; + let url = `/prcsFclt/ruseInfo/reg/${page}/${DEFAULT_LIST_SIZE}/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`; + if (searchTxt?.trim()) { + url += `?swtrmtFcltNm=${encodeURIComponent(searchTxt.trim())}`; } - const rainRes = await postFectch(url, data, fetchType.COMMON); // fo + const fcltRes = await getFetchWithCount(fetchType.COMMON, url); // fo - const hasuList = rainRes.data.map((item: any) => { + const hasuList = fcltRes.data.map((item: any) => { return { ...item, fctCpc: Number(item.fctCpc) || 0, - ruse: item.ruse === false ? "N" : "Y", + ruse: item?.bscFctCd && wrBscFctCdSet.has(item.bscFctCd) ? "Y" : "N", }; }); - return { data: hasuList, count: rainRes.count }; + return { data: hasuList, count: fcltRes.count }; }; const initFetch = async (ruseFcltPk: string | undefined) => { @@ -172,12 +195,12 @@ export const writeRgtrAndMdfr = (data: customPbnst[]) => { return data; }; -export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { +export const pubFacActionsExport: (set: any, get: any) => pubFacActions = (set: any, get: any) => { return { // 하수처리시설을 조회한다(운영실태) - pubHasuInit: async (page: string, sggOrCity: string) => { + pubHasuInit: async (page: number, sggOrCity: string) => { try { - const pbnstHasuRes = await hasuListFetch(page, sggOrCity); // /prcsFclt/ruseInfo + const pbnstHasuRes = await hasuListFetch(get, set, page, sggOrCity); // /prcsFclt/ruseInfo return set((state: pubFacState) => ({ ...state, pbnst: { @@ -192,12 +215,12 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { }, // 하수처리시설을 조회한다(운영실태) (처리시설명 포함) pubHasuSearch: async ( - page: string, + page: number, sggOrCity: string, searchTxt: string ) => { try { - const pbnstHasuRes = await hasuListFetch(page, sggOrCity, searchTxt); + const pbnstHasuRes = await hasuListFetch(get, set, page, sggOrCity, searchTxt); return set((state: pubFacState) => ({ ...state, pbnst: { diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx index 35a3fb3d7..f89320c45 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx @@ -47,7 +47,7 @@ export type pbnstSttnState = { pbnstSttnResCount: number; excelList: []; fclts: string[]; - isInit: boolean; + isInit: boolean; // wr 하수재이용시설 조회했는지 점검 }; }; @@ -254,6 +254,7 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( return set((state: pbnstSttnState) => ({ pbnstSttn: { ...state.pbnstSttn, + isInit: true, // wr 하수재이용시설을 조회 여부?? fclts: fclts, }, })); diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index 3024d1cb1..c90fc18bf 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -346,7 +346,7 @@ export const StoreZus = ( return { ...initState, ...comFcltSearchAction(set), - ...pubFacActionsExport(set), + ...pubFacActionsExport(set, get), ...rainFacActionsExport(set), ...rswtFacActionsExport(set), ...matDmdActionsExport(set), diff --git a/apps/wr/components/lib/func/common.tsx b/apps/wr/components/lib/func/common.tsx index 1d9d4a43e..7afeda993 100644 --- a/apps/wr/components/lib/func/common.tsx +++ b/apps/wr/components/lib/func/common.tsx @@ -66,9 +66,9 @@ export const tableData = (data: any, code: comCodeState) => { ...item, sggText: sggText ?? "없음", cityText: cityText ?? "없음", - bdsgUsgText: bdsgUsgText, - lawTrgtYn: lawTrgtYn, - grntchBdstCertGrdText: grntchBdstCertGrdText, + bdsgUsgText: bdsgUsgText, // 건물용도명 + lawTrgtYn: lawTrgtYn, // 법정대상 여부 + grntchBdstCertGrdText: grntchBdstCertGrdText, // 녹색건축인증등급명 }; } else { returnData = { From 84013993c9528cb6ae58cd72cb9586817da176ad Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 5 Jun 2026 17:18:14 +0900 Subject: [PATCH 06/97] =?UTF-8?q?[as]=20feat:=20=EA=B3=B5=EC=A2=85?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=ED=9A=8C=EA=B3=84=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=ED=95=98=EC=9C=84=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/next-env.d.ts | 6 -- .../lib/acntg-se-grid.ts | 31 ++++++-- .../lib/use-acntg-se-rows.ts | 78 +++++++++---------- .../ui/acntg-code-management-page.tsx | 66 ++++++++-------- .../acntg-code-management/ui/acntg-se-row.tsx | 2 +- .../lib/cnstp-se-grid.ts | 31 ++++++-- .../lib/use-cnstp-se-rows.ts | 68 ++++++++-------- .../ui/cnstp-code-management-page.tsx | 66 ++++++++-------- .../cnstp-code-management/ui/cnstp-se-row.tsx | 2 +- 9 files changed, 193 insertions(+), 157 deletions(-) delete mode 100644 apps/as/next-env.d.ts diff --git a/apps/as/next-env.d.ts b/apps/as/next-env.d.ts deleted file mode 100644 index 3cd7048ed..000000000 --- a/apps/as/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts index 1b7eaa6bf..c0ca7abc7 100644 --- a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts +++ b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts @@ -145,6 +145,15 @@ export function collectDescendantRowKeys( return result; } +export function insertRootSlot( + structure: AcntgDisplaySlot[], + slot: AcntgDisplaySlot, +): AcntgDisplaySlot[] { + const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null); + if (firstRootIndex === -1) return [slot, ...structure]; + return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)]; +} + export function insertChildSlot( structure: AcntgDisplaySlot[], parentRowKey: string, @@ -153,12 +162,7 @@ export function insertChildSlot( const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey); if (parentIndex === -1) return [...structure, slot]; - const parentDepth = structure[parentIndex].depth; - let insertAt = parentIndex + 1; - while (insertAt < structure.length && structure[insertAt].depth > parentDepth) { - insertAt += 1; - } - + const insertAt = parentIndex + 1; return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)]; } @@ -214,6 +218,21 @@ export function markAcntgRowsDeleted( return { ...state, deletedKeys }; } +export function validateDraftParentAcntgSeCd( + state: AcntgSeGridState, + rowKey: string, +): string | null { + const draft = state.drafts[rowKey]; + if (!draft?.parentRowKey) return null; + + const parent = mergeAcntgRow(state, draft.parentRowKey); + if (!parent?.acntgSeCd?.trim()) { + return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요."; + } + + return null; +} + export function removeDraftFromGrid(state: AcntgSeGridState, rowKey: string): AcntgSeGridState { const { [rowKey]: _removed, ...restDrafts } = state.drafts; const keysToRemove = collectDescendantRowKeys(state.structure, rowKey); diff --git a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts index a8c9ebc72..5fd9f85c5 100644 --- a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts +++ b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts @@ -13,11 +13,13 @@ import { hasAcntgGridPendingChanges, hydrateGridFromServer, insertChildSlot, + insertRootSlot, markAcntgRowsDeleted, mergeAcntgRow, mergeAllAcntgRows, patchAcntgGridRow, removeDraftFromGrid, + validateDraftParentAcntgSeCd, } from "./acntg-se-grid"; import { buildSaveMultiPayload } from "./build-save-multi-payload"; @@ -81,51 +83,43 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow useYn: "Y", }, }, - structure: [...prev.structure, { rowKey, depth: 0, parentRowKey: null }], + structure: insertRootSlot(prev.structure, { rowKey, depth: 0, parentRowKey: null }), })); }, []); - const addChildRow = useCallback( - (parent: AcntgSeRow) => { - if (parent.isNew) { - onAlert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다."); - return; - } + const addChildRow = useCallback((parent: AcntgSeRow) => { + const rowKey = createDraftRowKey(); + const slot = { + rowKey, + depth: 0, + parentRowKey: parent.rowKey, + }; - const rowKey = createDraftRowKey(); - const slot = { - rowKey, - depth: 0, - parentRowKey: parent.rowKey, - }; + setGrid((prev) => { + const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); + const depth = (parentSlot?.depth ?? 0) + 1; - setGrid((prev) => { - const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); - const depth = (parentSlot?.depth ?? 0) + 1; - - return { - ...prev, - drafts: { - ...prev.drafts, - [rowKey]: { - rowKey, - parentRowKey: parent.rowKey, - cdLvNo: (parent.cdLvNo ?? 1) + 1, - acntgSeCd: "", - acntgSeCdNm: "", - expln: "", - useYn: "Y", - }, + return { + ...prev, + drafts: { + ...prev.drafts, + [rowKey]: { + rowKey, + parentRowKey: parent.rowKey, + cdLvNo: (parent.cdLvNo ?? 1) + 1, + acntgSeCd: "", + acntgSeCdNm: "", + expln: "", + useYn: "Y", }, - structure: insertChildSlot(prev.structure, parent.rowKey, { - ...slot, - depth, - }), - }; - }); - }, - [onAlert], - ); + }, + structure: insertChildSlot(prev.structure, parent.rowKey, { + ...slot, + depth, + }), + }; + }); + }, []); const removeDraft = useCallback((rowKey: string) => { setGrid((prev) => removeDraftFromGrid(prev, rowKey)); @@ -146,6 +140,12 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow for (const row of mergedRows) { if (!row.isDeleted && (row.isNew || row.isDirty)) { + const parentCodeError = validateDraftParentAcntgSeCd(grid, row.rowKey); + if (parentCodeError) { + onAlert(parentCodeError); + return false; + } + const fieldError = validateAcntgSeRow(row); if (fieldError) { onAlert(fieldError); diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx index 07594e1c9..d480ced59 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx @@ -105,38 +105,40 @@ function AcntgCodeManagementPage() { 등록된 코드가 없습니다. [코드 추가]로 1단계 코드를 등록하세요.

) : ( - - - - - - - - - - - - 코드 - 코드명 - 설명 - 사용 여부 - 구분 - 관리 - - - - {displayRows.map(({ row, depth }) => ( - updateRow(row.rowKey, patch)} - onAddChild={() => addChildRow(row)} - onDelete={() => handleDelete(row)} - /> - ))} - -
+
+ + + + + + + + + + + + 코드 + 코드명 + 설명 + 사용 여부 + 구분 + 관리 + + + + {displayRows.map(({ row, depth }) => ( + updateRow(row.rowKey, patch)} + onAddChild={() => addChildRow(row)} + onDelete={() => handleDelete(row)} + /> + ))} + +
+
)}
diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx index a8011e6b8..69b53ec60 100644 --- a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx +++ b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx @@ -85,7 +85,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn type="button" size="sm" color="white" - disabled={row.isNew || !canAddChildRow()} + disabled={!canAddChildRow()} onClick={onAddChild} > 하위추가 diff --git a/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts b/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts index 63d9bedbd..6b1bd99f9 100644 --- a/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts +++ b/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts @@ -145,6 +145,15 @@ export function collectDescendantRowKeys( return result; } +export function insertRootSlot( + structure: CnstpDisplaySlot[], + slot: CnstpDisplaySlot, +): CnstpDisplaySlot[] { + const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null); + if (firstRootIndex === -1) return [slot, ...structure]; + return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)]; +} + export function insertChildSlot( structure: CnstpDisplaySlot[], parentRowKey: string, @@ -153,12 +162,7 @@ export function insertChildSlot( const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey); if (parentIndex === -1) return [...structure, slot]; - const parentDepth = structure[parentIndex].depth; - let insertAt = parentIndex + 1; - while (insertAt < structure.length && structure[insertAt].depth > parentDepth) { - insertAt += 1; - } - + const insertAt = parentIndex + 1; return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)]; } @@ -214,6 +218,21 @@ export function markCnstpRowsDeleted( return { ...state, deletedKeys }; } +export function validateDraftParentCnstpSeCd( + state: CnstpSeGridState, + rowKey: string, +): string | null { + const draft = state.drafts[rowKey]; + if (!draft?.parentRowKey) return null; + + const parent = mergeCnstpRow(state, draft.parentRowKey); + if (!parent?.cnstpSeCd?.trim()) { + return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요."; + } + + return null; +} + export function removeDraftFromGrid(state: CnstpSeGridState, rowKey: string): CnstpSeGridState { const { [rowKey]: _removed, ...restDrafts } = state.drafts; const keysToRemove = collectDescendantRowKeys(state.structure, rowKey); diff --git a/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts b/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts index fe1976e13..7e4320c8f 100644 --- a/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts +++ b/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts @@ -14,11 +14,13 @@ import { hasCnstpGridPendingChanges, hydrateGridFromServer, insertChildSlot, + insertRootSlot, markCnstpRowsDeleted, mergeAllCnstpRows, mergeCnstpRow, patchCnstpGridRow, removeDraftFromGrid, + validateDraftParentCnstpSeCd, } from "./cnstp-se-grid"; function validateCnstpSeRow(row: CnstpSeRow): string | null { @@ -80,47 +82,39 @@ export function useCnstpSeRows({ serverContent, onAlert, onSave }: UseCnstpSeRow useYn: "Y", }, }, - structure: [...prev.structure, { rowKey, depth: 0, parentRowKey: null }], + structure: insertRootSlot(prev.structure, { rowKey, depth: 0, parentRowKey: null }), })); }, []); - const addChildRow = useCallback( - (parent: CnstpSeRow) => { - if (parent.isNew) { - onAlert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다."); - return; - } + const addChildRow = useCallback((parent: CnstpSeRow) => { + const rowKey = createDraftRowKey(); - const rowKey = createDraftRowKey(); + setGrid((prev) => { + const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); + const depth = (parentSlot?.depth ?? 0) + 1; - setGrid((prev) => { - const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey); - const depth = (parentSlot?.depth ?? 0) + 1; - - return { - ...prev, - drafts: { - ...prev.drafts, - [rowKey]: { - rowKey, - parentRowKey: parent.rowKey, - cdLvNo: (parent.cdLvNo ?? 1) + 1, - cnstpSeCd: "", - cnstpSeCdNm: "", - expln: "", - useYn: "Y", - }, - }, - structure: insertChildSlot(prev.structure, parent.rowKey, { + return { + ...prev, + drafts: { + ...prev.drafts, + [rowKey]: { rowKey, - depth, parentRowKey: parent.rowKey, - }), - }; - }); - }, - [onAlert], - ); + cdLvNo: (parent.cdLvNo ?? 1) + 1, + cnstpSeCd: "", + cnstpSeCdNm: "", + expln: "", + useYn: "Y", + }, + }, + structure: insertChildSlot(prev.structure, parent.rowKey, { + rowKey, + depth, + parentRowKey: parent.rowKey, + }), + }; + }); + }, []); const removeDraft = useCallback((rowKey: string) => { setGrid((prev) => removeDraftFromGrid(prev, rowKey)); @@ -141,6 +135,12 @@ export function useCnstpSeRows({ serverContent, onAlert, onSave }: UseCnstpSeRow for (const row of mergedRows) { if (!row.isDeleted && (row.isNew || row.isDirty)) { + const parentCodeError = validateDraftParentCnstpSeCd(grid, row.rowKey); + if (parentCodeError) { + onAlert(parentCodeError); + return false; + } + const fieldError = validateCnstpSeRow(row); if (fieldError) { onAlert(fieldError); diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx index 68b0bf8a3..25ad711cb 100644 --- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx @@ -105,38 +105,40 @@ function CnstpCodeManagementPage() { 등록된 코드가 없습니다. [코드 추가]로 1단계 코드를 등록하세요.

) : ( - - - - - - - - - - - - 코드 - 코드명 - 설명 - 사용 여부 - 구분 - 관리 - - - - {displayRows.map(({ row, depth }) => ( - updateRow(row.rowKey, patch)} - onAddChild={() => addChildRow(row)} - onDelete={() => handleDelete(row)} - /> - ))} - -
+
+ + + + + + + + + + + + 코드 + 코드명 + 설명 + 사용 여부 + 구분 + 관리 + + + + {displayRows.map(({ row, depth }) => ( + updateRow(row.rowKey, patch)} + onAddChild={() => addChildRow(row)} + onDelete={() => handleDelete(row)} + /> + ))} + +
+
)}
diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx index 1588c12c5..090e7d2ba 100644 --- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx +++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx @@ -85,7 +85,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns type="button" size="sm" color="white" - disabled={row.isNew || !canAddChildRow()} + disabled={!canAddChildRow()} onClick={onAddChild} > 하위추가 From 6df6e612bcc80f86efa5e61348e09762dfc41c04 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 5 Jun 2026 17:44:51 +0900 Subject: [PATCH 07/97] =?UTF-8?q?wr:=20=EA=B3=B5=ED=86=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/api/common-codes/route.ts | 207 +++++++++++++++++++++ apps/wr/app/store/common/code/index.ts | 76 +++++++- apps/wr/src/app/providers/app-provider.tsx | 19 +- 3 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 apps/wr/app/api/common-codes/route.ts diff --git a/apps/wr/app/api/common-codes/route.ts b/apps/wr/app/api/common-codes/route.ts new file mode 100644 index 000000000..ded38eadf --- /dev/null +++ b/apps/wr/app/api/common-codes/route.ts @@ -0,0 +1,207 @@ +import { SITE_ID } from "@/src/shared/config"; +import { NextResponse } from "next/server"; + +export const revalidate = 60 * 60 * 24; + +type SelectDataType = { + text: string; + val: string; +}; + +type EnvPartSelectDataType = SelectDataType & { + adstDliStrCd: string; +}; + +const codeMapping = { + WR001: "bdstUsgCd", + WR002: "rswtBdstType", + WR003: "rainFcltCd", + WR004: "bdstGrntchClass", + WR005: "rswtPrscCdMthdCd", + WR006: "orgwtKndCd", + WR007: "ruseMngMtd", + WR008: "rusePrcsMthdCd", + WR009: "erctUswtrPrcsMeth", + WR010: "ruseFcltSpot", + WR011: "pplQulity", + WR012: "pplLenght", + WR013: "bdstFilterYn", + WR014: "rainRswtMng", + WR015: "rainFcltFlowRate", + WR016: "dmdUszCd", + WR017: "sewageInFcltUse", + WR018: "sewageOutFclttUse", + WR019: "htdPowerMthdCd", + WR020: "htdUtztnUsgCd", +} as const; + +const defaultHeaders = { + accept: "application/json", + "X-Site-Id": SITE_ID, +}; + +function getServerBaseUrl() { + const port = process.env.PORT ?? process.env.NEXT_PUBLIC_PORT ?? "3000"; + const proxyHost = process.env.NEXT_PUBLIC_PROXY_HOST; + + if (process.env.NODE_ENV === "production" && process.env.HOSTNAME) { + return `http://${process.env.HOSTNAME}:${port}`; + } + + return proxyHost || `http://localhost:${port}`; +} + +async function fetchJson(url: string, cookieHeader?: string) { + const response = await fetch(url, { + method: "GET", + headers: { + ...defaultHeaders, + ...(cookieHeader ? { cookie: cookieHeader } : {}), + }, + next: { revalidate }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status}`); + } + + return response.json(); +} + +async function fetchOperationData(path: string, cookieHeader?: string) { + const json = await fetchJson( + `${getServerBaseUrl()}/operation-service${path}`, + cookieHeader + ); + return json.data; +} + +async function fetchCmsData(path: string, cookieHeader?: string) { + return fetchJson(`${getServerBaseUrl()}${path}`, cookieHeader); +} + +async function fetchCommonCodesByKeys(keys: string[], cookieHeader?: string) { + const targets = [...new Set(keys)].filter( + (cd) => Boolean(codeMapping[cd as keyof typeof codeMapping]) + ); + + const entries = await Promise.all( + targets.map(async (cd) => { + const mapping = codeMapping[cd as keyof typeof codeMapping]; + const lwCd = await fetchCmsData( + `/cms-service/api/v1/lwpo-cds/up-cd/${cd}`, + cookieHeader + ); + const list = Array.isArray(lwCd) ? lwCd : (lwCd.content ?? []); + + return [ + mapping, + list.map((it: any) => ({ + text: it.lwpoCdNm, + val: it.lwpoCd, + })), + ] as const; + }) + ); + + return Object.fromEntries(entries) as Record; +} + +export async function GET(request: Request) { + try { + const cookieHeader = request.headers.get("cookie") ?? ""; + const [sgg, city, rvbsn, mdmbsn, lgbsn, lclgvrnmnt, upCd] = + await Promise.all([ + fetchOperationData("/addr/sgg", cookieHeader), + fetchOperationData("/addr/city", cookieHeader), + fetchOperationData("/addr/sarea", cookieHeader), + fetchOperationData("/addr/midArea", cookieHeader), + fetchOperationData("/addr/largeArea", cookieHeader), + fetchOperationData("/addr/govArea", cookieHeader), + fetchCmsData( + "/cms-service/api/v1/up-codes?keyword=&upCd=&cdSe=&useYn=Y&siteId=WR&page=0&size=100&sort=string", + cookieHeader + ), + ]); + + const upWrCode: string[] = (upCd.content ?? []) + .filter((item: any) => item.upCd.slice(0, 2) === "WR") + .map((cd: any) => cd.upCd); + + const lwCds = await fetchCommonCodesByKeys(upWrCode, cookieHeader); + + const payload = { + sggCd: [ + ...((sgg ?? []).map((item: any) => ({ + text: item.gugun || "", + val: String(item.adstDstrCd) || "", + })) as SelectDataType[]), + ], + cityCd: [ + { val: "0", text: "전체" }, + ...((city ?? []).map((item: any) => ({ + val: item.adstDstrCd, + text: item.sido, + })) as SelectDataType[]), + ], + rvbsnCd: [ + { val: "0", text: "전체" }, + ...((rvbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.rvbsn, + })) as SelectDataType[]), + ], + mdmbsnCd: [ + { val: "0", text: "전체" }, + ...((mdmbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.mdmbsn, + })) as SelectDataType[]), + ], + lgbsnCd: [ + { val: "0", text: "전체" }, + ...((lgbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.lgbsn, + })) as SelectDataType[]), + ], + lclgvrnmntCd: [ + { val: "0", text: "전체", adstDliStrCd: "" }, + ...((lclgvrnmnt ?? []).map((item: any) => ({ + val: item.lclgvrnmntCd, + text: item.lclgvrnmntNm, + adstDliStrCd: item.adstDliStrCd || "", + })) as EnvPartSelectDataType[]), + ], + rainFcltCd: lwCds["rainFcltCd"] ?? [], + bdstUsgCd: lwCds["bdstUsgCd"] ?? [], + orgwtKndCd: lwCds["orgwtKndCd"] ?? [], + pplQulity: lwCds["pplQulity"] ?? [], + pplLenght: lwCds["pplLenght"] ?? [], + dmdUszCd: lwCds["dmdUszCd"] ?? [], + sewageOutFclttUse: lwCds["sewageOutFclttUse"] ?? [], + rainRswtMng: lwCds["rainRswtMng"] ?? [], + rainFcltFlowRate: lwCds["rainFcltFlowRate"] ?? [], + ruseMngMtd: lwCds["ruseMngMtd"] ?? [], + rusePrcsMthdCd: lwCds["rusePrcsMthdCd"] ?? [], + erctUswtrPrcsMeth: lwCds["erctUswtrPrcsMeth"] ?? [], + bdstGrntchClass: lwCds["bdstGrntchClass"] ?? [], + htdPowerMthdCd: lwCds["htdPowerMthdCd"] ?? [], + htdUtztnUsgCd: lwCds["htdUtztnUsgCd"] ?? [], + rswtBdstType: lwCds["rswtBdstType"] ?? [], + bdstTotalUsgType: (lwCds["bdstUsgCd"] ?? []).concat(lwCds["rswtBdstType"] ?? []), + }; + + return NextResponse.json(payload, { + headers: { + "Cache-Control": "s-maxage=86400, stale-while-revalidate=3600", + }, + }); + } catch (error) { + console.error("Failed to load common codes:", error); + return NextResponse.json( + { message: "Failed to load common codes" }, + { status: 500 } + ); + } +} diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index 4f7896de0..0ddb7fc8c 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -371,18 +371,80 @@ export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any }, initCommonCodes: async () => { if (get().code.isInit) return; + const response = await fetch("/api/common-codes", { + method: "GET", + headers: { + accept: "application/json", + }, + }); - await getSggCd(set); - await getCityCd(set); - await getRegionCd(set); - await getMiddleRegionCd(set); - await getUnitWaterShedCd(set); - await getEnvPartCd(set); - await getCommonUpCd(set); + if (!response.ok) { + throw new Error(`공통 코드 조회 실패: ${response.status}`); + } + + const data = await response.json(); + + lwCds["rainFcltCd"] = data.rainFcltCd ?? lwCds["rainFcltCd"] ?? []; + lwCds["bdstUsgCd"] = data.bdstUsgCd ?? lwCds["bdstUsgCd"] ?? []; + lwCds["orgwtKndCd"] = data.orgwtKndCd ?? lwCds["orgwtKndCd"] ?? []; + lwCds["pplQulity"] = data.pplQulity ?? lwCds["pplQulity"] ?? []; + lwCds["pplLenght"] = data.pplLenght ?? lwCds["pplLenght"] ?? []; + lwCds["dmdUszCd"] = data.dmdUszCd ?? lwCds["dmdUszCd"] ?? []; + lwCds["sewageOutFclttUse"] = + data.sewageOutFclttUse ?? lwCds["sewageOutFclttUse"] ?? []; + lwCds["rainRswtMng"] = data.rainRswtMng ?? lwCds["rainRswtMng"] ?? []; + lwCds["rainFcltFlowRate"] = + data.rainFcltFlowRate ?? lwCds["rainFcltFlowRate"] ?? []; + lwCds["ruseMngMtd"] = data.ruseMngMtd ?? lwCds["ruseMngMtd"] ?? []; + lwCds["rusePrcsMthdCd"] = + data.rusePrcsMthdCd ?? lwCds["rusePrcsMthdCd"] ?? []; + lwCds["erctUswtrPrcsMeth"] = + data.erctUswtrPrcsMeth ?? lwCds["erctUswtrPrcsMeth"] ?? []; + lwCds["bdstGrntchClass"] = + data.bdstGrntchClass ?? lwCds["bdstGrntchClass"] ?? []; + lwCds["htdPowerMthdCd"] = + data.htdPowerMthdCd ?? lwCds["htdPowerMthdCd"] ?? []; + lwCds["htdUtztnUsgCd"] = + data.htdUtztnUsgCd ?? lwCds["htdUtztnUsgCd"] ?? []; + lwCds["rswtBdstType"] = + data.rswtBdstType ?? lwCds["rswtBdstType"] ?? []; + lwCds["bdstTotalUsgType"] = + data.bdstTotalUsgType ?? lwCds["bdstTotalUsgType"] ?? []; set((state) => ({ code: { ...state.code, + sggCd: data.sggCd ?? state.code.sggCd, + cityCd: data.cityCd ?? state.code.cityCd, + rvbsnCd: data.rvbsnCd ?? state.code.rvbsnCd, + mdmbsnCd: data.mdmbsnCd ?? state.code.mdmbsnCd, + lgbsnCd: data.lgbsnCd ?? state.code.lgbsnCd, + lclgvrnmntCd: data.lclgvrnmntCd ?? state.code.lclgvrnmntCd, + rainFcltCd: data.rainFcltCd ?? state.code.rainFcltCd, + bdstUsgCd: data.bdstUsgCd ?? state.code.bdstUsgCd, + orgwtKndCd: data.orgwtKndCd ?? state.code.orgwtKndCd, + pplQulity: data.pplQulity ?? state.code.pplQulity, + pplLenght: data.pplLenght ?? state.code.pplLenght, + dmdUszCd: data.dmdUszCd ?? state.code.dmdUszCd, + sewageOutFclttUse: + data.sewageOutFclttUse ?? state.code.sewageOutFclttUse, + rainRswtMng: data.rainRswtMng ?? state.code.rainRswtMng, + rainFcltFlowRate: + data.rainFcltFlowRate ?? state.code.rainFcltFlowRate, + ruseMngMtd: data.ruseMngMtd ?? state.code.ruseMngMtd, + rusePrcsMthdCd: + data.rusePrcsMthdCd ?? state.code.rusePrcsMthdCd, + erctUswtrPrcsMeth: + data.erctUswtrPrcsMeth ?? state.code.erctUswtrPrcsMeth, + bdstGrntchClass: + data.bdstGrntchClass ?? state.code.bdstGrntchClass, + htdPowerMthdCd: + data.htdPowerMthdCd ?? state.code.htdPowerMthdCd, + htdUtztnUsgCd: + data.htdUtztnUsgCd ?? state.code.htdUtztnUsgCd, + rswtBdstType: data.rswtBdstType ?? state.code.rswtBdstType, + bdstTotalUsgType: + data.bdstTotalUsgType ?? state.code.bdstTotalUsgType, isInit: true, }, })); diff --git a/apps/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx index 63df8349d..33f2e728a 100644 --- a/apps/wr/src/app/providers/app-provider.tsx +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect } from "react"; import { DndProvider } from "react-dnd"; import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -16,7 +17,10 @@ import i18next from "i18next"; import { I18nextProvider } from "react-i18next"; import { Toaster } from "react-hot-toast"; import koBase from "@/public/locales/ko/message.json"; -import { StoreProvider as WrStoreProvider } from "@/app/store"; +import { + StoreProvider as WrStoreProvider, + useWrStore, +} from "@/app/store"; //import { LoginApi } from "@/src/entities/auth"; //import { useEffect } from "react"; import { setCookie } from "@/src/shared/lib/cookie"; @@ -34,6 +38,18 @@ i18n.init({ }, }); +function CommonCodeInitializer() { + const isInit = useWrStore((state) => state.code.isInit); + const initCommonCodes = useWrStore((state) => state.initCommonCodes); + + useEffect(() => { + if (isInit) return; + initCommonCodes(); + }, [isInit, initCommonCodes]); + + return null; +} + const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); return ( @@ -49,6 +65,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { > + {children} From 66db81071e0b5bd158952941a8ffd34200935e3e Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 8 Jun 2026 09:06:58 +0900 Subject: [PATCH 08/97] =?UTF-8?q?wr:=20=EB=B9=8C=EB=93=9C=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/api/common-codes/route.ts | 3 ++- apps/wr/app/store/map/pbnstMap/index.tsx | 10 ++++++---- apps/wr/app/store/store.tsx | 9 +-------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/wr/app/api/common-codes/route.ts b/apps/wr/app/api/common-codes/route.ts index ded38eadf..aa62b261b 100644 --- a/apps/wr/app/api/common-codes/route.ts +++ b/apps/wr/app/api/common-codes/route.ts @@ -1,7 +1,8 @@ +export const dynamic = "force-dynamic"; import { SITE_ID } from "@/src/shared/config"; import { NextResponse } from "next/server"; -export const revalidate = 60 * 60 * 24; +export const revalidate = 86400; //60 * 60 * 24; type SelectDataType = { text: string; diff --git a/apps/wr/app/store/map/pbnstMap/index.tsx b/apps/wr/app/store/map/pbnstMap/index.tsx index 18f9abfe2..80eb2697a 100644 --- a/apps/wr/app/store/map/pbnstMap/index.tsx +++ b/apps/wr/app/store/map/pbnstMap/index.tsx @@ -9,13 +9,15 @@ import { excelHasuMapParsing } from "./excel"; export const pbnMapTableSize = 20; const hasuListFetch = async ( + get: any, + set: any, page: number, pageTotal: number, reurnValue: ReturnType, searchNm?: string ) => { let url = `/prcsFclt/ruseInfo/${page}/${pageTotal}/`; - const checkWrRegListRes = await checkWrRegList(); + const checkWrRegListRes = await checkWrRegList(get, set); if (reurnValue.partValue === "A") { url += `${reurnValue.partValue}/${ @@ -92,7 +94,7 @@ export const pbnMapStateExport: pbnMapState = { }, }; -export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { +export const pbnMapActionsExport: (set: any, get: any) => pbnMapActions = (set: any, get: any) => { return { pbnMapInit: async ( page: number, @@ -100,7 +102,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { reurnValue: ReturnType ) => { try { - const pubMapRes = await hasuListFetch(page, pageTotal, reurnValue); + const pubMapRes = await hasuListFetch(get, set, page, pageTotal, reurnValue); set((state: pbnMapState) => { return { ...state, @@ -144,7 +146,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { txt: string, ) => { try { - const hasuMapRes = await hasuListFetch(page, pageTotal, reurnValue, txt); + const hasuMapRes = await hasuListFetch(get, set, page, pageTotal, reurnValue, txt); if (isExcel) { const hasuMapExcelList = excelHasuMapParsing( hasuMapRes diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index c90fc18bf..268be198b 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -2,13 +2,6 @@ import { createStore } from "zustand/vanilla"; import { codeActionExport, comCodeStateExport, - getCityCd, - getCommonUpCd, - getEnvPartCd, - getMiddleRegionCd, - getRegionCd, - getSggCd, - getUnitWaterShedCd, type comCodeState, type comCodeType, } from "./common/code"; @@ -373,7 +366,7 @@ export const StoreZus = ( ...rswtStatActionsExport(set), ...authActionsExport(set,get), ...commonMapActionsExport(set), - ...pbnMapActionsExport(set), + ...pbnMapActionsExport(set, get), ...rainMapActionsExport(set), ...rswtMapActionsExport(set), ...htdfMapActionsExport(set), From 8c1a976b5bc23b9dbcb5e14e43a86a9deb8a0701 Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 8 Jun 2026 11:02:11 +0900 Subject: [PATCH 09/97] wr: isInit --- .../ruseFcltMng/fcltSttn/pbnstSttn/page.tsx | 27 +++++++++++++------ .../ruseFcltMng/fcltSttn/pbnstSttn/index.tsx | 24 +++++++++-------- apps/wr/src/app/providers/app-provider.tsx | 7 ++++- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx index a481f66a1..0e49c4c6f 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx @@ -134,19 +134,30 @@ export default function PbnstSttn() { useEffect(() => { getLaeSeCdCheck(); useInitRef.current = true; + if (!pbnstSttn.isFcltsLoaded) { + getFlcts(); // 하수재이용시설 조회 + } }, []); useEffect(() => { - if (!pbnstSttn.isInit) { - getFlcts(); // 하수재이용시설 조회 - } - }, [pbnstSttn.isInit]); - - useEffect(() => { - if (useInitRef.current === true && laeSeCd !== undefined && fclts.length > 0) { + if ( + useInitRef.current === true && + laeSeCd !== undefined && + pbnstSttn.isFcltsLoaded && + !pbnstSttn.isTableLoaded && + fclts.length > 0 + ) { pbnSttnInit("1", String(DEFAULT_LIST_SIZE), getFixedSearchValue(ref.current), searchYear, fclts); } - }, [fclts, laeSeCd, searchYear, pbnSttnInit, getFixedSearchValue]); + }, [ + fclts, + laeSeCd, + pbnstSttn.isFcltsLoaded, + pbnstSttn.isTableLoaded, + searchYear, + pbnSttnInit, + getFixedSearchValue, + ]); useEffect(() => { if (pbnstSttnRes && pbnstSttnRes.length > 0) { diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx index f89320c45..b1cf957e6 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx @@ -47,7 +47,8 @@ export type pbnstSttnState = { pbnstSttnResCount: number; excelList: []; fclts: string[]; - isInit: boolean; // wr 하수재이용시설 조회했는지 점검 + isFcltsLoaded: boolean; // wr 하수재이용시설 목록 조회 여부 + isTableLoaded: boolean; // 현황 테이블 초기 조회 여부 }; }; @@ -100,7 +101,8 @@ export const pbnstSttnStateExport: pbnstSttnState = { pbnstSearchData: [], excelList: [], fclts: [], - isInit: false, + isFcltsLoaded: false, + isTableLoaded: false, }, }; @@ -251,13 +253,13 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( //등록된 하수재이용시설을 조회한다. getFlcts: async () => { const fclts = await getFetch(fetchType.WR, "/reuseFc/pubFc"); - return set((state: pbnstSttnState) => ({ - pbnstSttn: { - ...state.pbnstSttn, - isInit: true, // wr 하수재이용시설을 조회 여부?? - fclts: fclts, - }, - })); + return set((state: pbnstSttnState) => ({ + pbnstSttn: { + ...state.pbnstSttn, + isFcltsLoaded: true, + fclts: fclts, + }, + })); }, //테이블 헤드를 조정한다. pbnstChSttnHead: async ( @@ -314,7 +316,7 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( ...state.pbnstSttn, pbnstSttnRes: pbnstSttnRes.data, pbnstSttnResCount: pbnstSttnRes.count, - // isInit: false, + isTableLoaded: true, }, }; }); @@ -348,8 +350,8 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( ...state.pbnstSttn, pbnstSttnRes: pbnstSttnRes.data, pbnstSttnResCount: pbnstSttnRes.count, - isInit: false, pbnstDetail: {} as customPbnst, + isTableLoaded: true, }, }; }); diff --git a/apps/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx index 33f2e728a..f54dfea52 100644 --- a/apps/wr/src/app/providers/app-provider.tsx +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -21,6 +21,7 @@ import { StoreProvider as WrStoreProvider, useWrStore, } from "@/app/store"; +import { usePathname } from "next/navigation"; //import { LoginApi } from "@/src/entities/auth"; //import { useEffect } from "react"; import { setCookie } from "@/src/shared/lib/cookie"; @@ -41,11 +42,15 @@ i18n.init({ function CommonCodeInitializer() { const isInit = useWrStore((state) => state.code.isInit); const initCommonCodes = useWrStore((state) => state.initCommonCodes); + const pathname = usePathname(); + const isLoginPage = + pathname === "/auth/login" || pathname?.startsWith("/auth/login/"); useEffect(() => { + if (isLoginPage) return; if (isInit) return; initCommonCodes(); - }, [isInit, initCommonCodes]); + }, [isInit, initCommonCodes, isLoginPage]); return null; } From 814365d7debc123c9c862f9867aa6299d655d670 Mon Sep 17 00:00:00 2001 From: ljpark Date: Mon, 8 Jun 2026 11:44:12 +0900 Subject: [PATCH 10/97] =?UTF-8?q?wr:=20common-codes=20error=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/api/common-codes/route.ts | 15 +++++++++++++-- apps/wr/app/store/common/code/index.ts | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/wr/app/api/common-codes/route.ts b/apps/wr/app/api/common-codes/route.ts index aa62b261b..ae0c175cd 100644 --- a/apps/wr/app/api/common-codes/route.ts +++ b/apps/wr/app/api/common-codes/route.ts @@ -63,7 +63,12 @@ async function fetchJson(url: string, cookieHeader?: string) { }); if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); + const responseText = await response.text(); + throw new Error( + `Failed to fetch ${url}: ${response.status} ${response.statusText}${ + responseText ? ` - ${responseText}` : "" + }` + ); } return response.json(); @@ -200,8 +205,14 @@ export async function GET(request: Request) { }); } catch (error) { console.error("Failed to load common codes:", error); + const message = + error instanceof Error ? error.message : "Failed to load common codes"; + return NextResponse.json( - { message: "Failed to load common codes" }, + { + message: "Failed to load common codes", + detail:message , + }, { status: 500 } ); } diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index 0ddb7fc8c..5e6600960 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -379,7 +379,24 @@ export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any }); if (!response.ok) { - throw new Error(`공통 코드 조회 실패: ${response.status}`); + let errorMessage = `공통 코드 조회 실패: ${response.status}`; + + try { + const errorBody = await response.json(); + if (errorBody?.detail) { + errorMessage += ` - ${errorBody.detail}`; + } else if (errorBody?.message) { + errorMessage += ` - ${errorBody.message}`; + } + } catch { + const errorText = await response.text(); + if (errorText) { + errorMessage += ` - ${errorText}`; + } + } + + console.error("initCommonCodes failed:", errorMessage); + throw new Error(errorMessage); } const data = await response.json(); From fc70963fa921969f4b8614faac28e06712ccbd85 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 8 Jun 2026 16:18:42 +0900 Subject: [PATCH 11/97] =?UTF-8?q?[as]=20feat:=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=9D=B8=EB=A0=A5=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A0=95=20&=20=EA=B2=B0=EC=9E=AC?= =?UTF-8?q?=EC=84=A0=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=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 --- apps/as/src/entities/rvw-mnpw/api/queries.ts | 3 +- .../rvw-mnpw/model/rvw-mnpw-interface.ts | 3 + .../lib/validators.ts | 7 +- .../model/reviewer-option.ts | 10 +- .../ui/aprvr-picker-dialog.tsx | 180 ++++++++---------- .../ui/elapr-apln-grid.tsx | 2 + .../ui/sortable-apln-row.tsx | 6 +- .../model/rvw-mnpw-block-config.ts | 7 +- 8 files changed, 104 insertions(+), 114 deletions(-) diff --git a/apps/as/src/entities/rvw-mnpw/api/queries.ts b/apps/as/src/entities/rvw-mnpw/api/queries.ts index 651906de0..6e075e4d5 100644 --- a/apps/as/src/entities/rvw-mnpw/api/queries.ts +++ b/apps/as/src/entities/rvw-mnpw/api/queries.ts @@ -12,10 +12,11 @@ import { rvwMnpwMngService } from "./rvw-mnpw-service"; const rvwMnpwMngBaseKey = [ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW] as const; -export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto) { +export function useRvwMnpwMngPage(params: RvwMnpwMngSearchDto, options?: { enabled?: boolean }) { return useQuery({ queryKey: [...rvwMnpwMngBaseKey, "findPage", params], queryFn: () => rvwMnpwMngService.findPage(params), + enabled: options?.enabled, }); } diff --git a/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts index 73aef5961..92cbff071 100644 --- a/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts +++ b/apps/as/src/entities/rvw-mnpw/model/rvw-mnpw-interface.ts @@ -7,6 +7,9 @@ export interface RvwMnpwMngResponseDto { ognzSn: number; userUuid: string; userUuidNm?: string; + ognzNm?: string; + jbpsSeNm?: string; + jbgdSeNm?: string; rgtr?: string; regDt?: string; } diff --git a/apps/as/src/pages/approval-line-management/lib/validators.ts b/apps/as/src/pages/approval-line-management/lib/validators.ts index c5fbe0b71..4e301afa9 100644 --- a/apps/as/src/pages/approval-line-management/lib/validators.ts +++ b/apps/as/src/pages/approval-line-management/lib/validators.ts @@ -1,10 +1,7 @@ import type { ElaprAplnRow } from "../model/apln-row-types"; -export const ATRZ_RECEIVER_CD = "ATRZ00"; -export const ATRZ_REVIEWER_1_CD = "ATRZ01"; -export const ATRZ_REVIEWER_2_CD = "ATRZ02"; -export const ATRZ_APPROVER_CD = "ATRZ03"; -export const ATRZ_FINAL_CD = "ATRZ99"; +const ATRZ_RECEIVER_CD = "ATRZ01"; +const ATRZ_FINAL_CD = "ATRZ99"; export function getVisibleRows(rows: ElaprAplnRow[]): ElaprAplnRow[] { return rows.filter((row) => !row.isDeleted); diff --git a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts index 3b4658acd..cb79bbf13 100644 --- a/apps/as/src/pages/approval-line-management/model/reviewer-option.ts +++ b/apps/as/src/pages/approval-line-management/model/reviewer-option.ts @@ -1,4 +1,4 @@ -import type { SelectedOgnzUser } from "@/widgets/ognz-user-tree"; +import type { RvwMnpwMngResponseDto } from "@/entities/rvw-mnpw"; export interface ReviewerOption { uuid: string; @@ -6,10 +6,10 @@ export interface ReviewerOption { deptNm: string; } -export function fromSelectedOgnzUser(user: SelectedOgnzUser): ReviewerOption { +export function fromRvwMnpwMng(item: RvwMnpwMngResponseDto): ReviewerOption { return { - uuid: user.userUuid, - name: user.userNm, - deptNm: user.ognzNm, + uuid: item.userUuid, + name: item.userUuidNm ?? "", + deptNm: item.ognzNm ?? "", }; } diff --git a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx index 7fbab9f17..38f7c810d 100644 --- a/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx +++ b/apps/as/src/pages/approval-line-management/ui/aprvr-picker-dialog.tsx @@ -1,14 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; - -import { useOgnzUserTree } from "@/entities/user-organization"; -import { - collectExpandedOgnzSnIds, - filterOgnzUserTree, - OgnzUserTree, - type SelectedOgnzUser, -} from "@/widgets/ognz-user-tree"; +import { useRvwMnpwMngPage } from "@/entities/rvw-mnpw"; import { Button, Dialog, @@ -16,113 +8,109 @@ import { DialogFooter, DialogHeader, DialogTitle, - Input, + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@common/components"; -import { fromSelectedOgnzUser, type ReviewerOption } from "../model/reviewer-option"; +import { fromRvwMnpwMng, type ReviewerOption } from "../model/reviewer-option"; + +const PAGE_SIZE = 500; interface AprvrPickerDialogProps { open: boolean; onOpenChange: (open: boolean) => void; + atrzSeComCd: string; onSelect: (reviewer: ReviewerOption) => void; } -function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogProps) { - const [keyword, setKeyword] = useState(""); - const [displayTree, setDisplayTree] = useState>([]); - const [expandedOgnzSnIds, setExpandedOgnzSnIds] = useState>(new Set()); - const [selectedUserId, setSelectedUserId] = useState(); - const [isSearchActive, setIsSearchActive] = useState(false); +function AprvrPickerDialog({ + open, + onOpenChange, + atrzSeComCd, + onSelect, +}: AprvrPickerDialogProps) { + const { data, isPending, isError } = useRvwMnpwMngPage( + { + atrzSeComCd, + page: 0, + size: PAGE_SIZE, + sort: "userUuidNm,asc", + }, + { enabled: open }, + ); - const { data, isPending, isError } = useOgnzUserTree({ enabled: open }); + const items = data?.content ?? []; - const resetState = useCallback(() => { - setKeyword(""); - setSelectedUserId(undefined); - setExpandedOgnzSnIds(new Set()); - setIsSearchActive(false); - setDisplayTree(data ?? []); - }, [data]); - - useEffect(() => { - if (!open) { - setKeyword(""); - setSelectedUserId(undefined); - setExpandedOgnzSnIds(new Set()); - setIsSearchActive(false); - return; - } - - setDisplayTree(data ?? []); - }, [open, data]); - - const handleToggle = (ognzSn: number, isExpanded: boolean) => { - setExpandedOgnzSnIds((prev) => { - const next = new Set(prev); - if (isExpanded) next.add(ognzSn); - else next.delete(ognzSn); - return next; - }); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const filtered = filterOgnzUserTree(data ?? [], keyword); - setDisplayTree(filtered); - setExpandedOgnzSnIds(collectExpandedOgnzSnIds(filtered)); - setIsSearchActive(keyword.trim().length > 0); - }; - - const handleReset = () => { - resetState(); - }; - - const handleSelect = (user: SelectedOgnzUser) => { - setSelectedUserId(user.userUuid); - onSelect(fromSelectedOgnzUser(user)); + const handleSelect = (item: (typeof items)[number]) => { + onSelect(fromRvwMnpwMng(item)); onOpenChange(false); }; return ( - + 검토인력 선택 -
-
-
- setKeyword(e.target.value)} - /> - - -
-
- -
- -
+
+ {isPending ? ( +
+ + +
+ ) : isError ? ( +

+ 검토인력 목록을 불러오지 못했습니다. +

+ ) : items.length === 0 ? ( +

+ 배치된 검토인력이 없습니다. +

+ ) : ( + + + + + + + + + + + 성명 + 부서 + 직위 + 직급 + 선택 + + + + {items.map((item) => ( + + + {item.userUuidNm || item.userUuid} + + {item.ognzNm || "-"} + {item.jbpsSeNm || "-"} + {item.jbgdSeNm || "-"} + + + + + ))} + +
+ )}
diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx index 1371db596..c2393715b 100644 --- a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx @@ -75,6 +75,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { const isDisabled = !elaprTmpltId; const serverRows = aplnQuery.data?.content ?? []; const canSave = hasUnsavedChanges(rows, serverRows); + const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; @@ -236,6 +237,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { diff --git a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx index 266fe0b45..fcb315bdb 100644 --- a/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx +++ b/apps/as/src/pages/approval-line-management/ui/sortable-apln-row.tsx @@ -36,7 +36,7 @@ function SortableAplnRow({ onChange, onPickAprvr, }: SortableAplnRowProps) { - const { data: atrzSeCodes } = useCodeList("ATRZSECD"); + const { data: atrzSeCodes } = useCodeList("ATRZSE"); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: row.rowKey, @@ -50,7 +50,7 @@ function SortableAplnRow({ }; const rowDisabled = isLocked && !isEditing; - const atrzSeLabel = row.atrzSeComNm ?? findCodeLabel(atrzSeCodes, row.atrzSeComCd); + const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd); return ( @@ -72,7 +72,7 @@ function SortableAplnRow({ {isEditing ? ( diff --git a/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts index 30dff1f37..5a6c681e1 100644 --- a/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts +++ b/apps/as/src/pages/rvw-mnpw-mng/model/rvw-mnpw-block-config.ts @@ -4,11 +4,10 @@ export interface RvwMnpwBlockConfig { atrzSeComCd: string; } -/** 결재선 관리 validators.ts ATRZ 코드와 동일. ATRZSE / ATRZSECD 배포 환경 코드표 최종 확인 필요 */ +/** 결재선 관리 validators.ts ATRZ 코드와 동일 */ export const RVW_MNPW_BLOCKS: RvwMnpwBlockConfig[] = [ - { id: "receiver", title: "접수", atrzSeComCd: "ATRZ00" }, - // TBD: ATRZ02(검토2) 별도 블록 필요 여부 — 현재 ATRZ01만 검토 블록으로 사용 - { id: "reviewer", title: "검토", atrzSeComCd: "ATRZ01" }, + { id: "receiver", title: "접수", atrzSeComCd: "ATRZ01" }, + { id: "reviewer", title: "검토", atrzSeComCd: "ATRZ02" }, { id: "approver", title: "승인", atrzSeComCd: "ATRZ03" }, { id: "final", title: "최종승인", atrzSeComCd: "ATRZ99" }, ]; From abf53afafc2ff933e667d375a26a52058f166275 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 8 Jun 2026 17:04:38 +0900 Subject: [PATCH 12/97] =?UTF-8?q?[as]=20chore:=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=9D=B8=EB=A0=A5=EA=B4=80=EB=A6=AC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=EC=97=90=20=EC=A7=81=EA=B8=89=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/ognz-user-tree-interface.ts | 2 ++ .../src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx | 14 ++++++++------ .../ognz-user-tree/ui/ognz-user-tree-node.tsx | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts b/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts index 71dcc5833..47e75d944 100644 --- a/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts +++ b/apps/as/src/entities/user-organization/model/ognz-user-tree-interface.ts @@ -4,6 +4,8 @@ export interface OgnzUserTreeUserDto { userNm: string; jbpsSeCd: string; jbgdSeCd: string; + jbpsSeNm: string; + jbgdSeNm: string; } /** OgnzUserTreeResponseDto */ diff --git a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx index e03918de6..b3140e67a 100644 --- a/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx +++ b/apps/as/src/pages/rvw-mnpw-mng/ui/rvw-mnpw-block.tsx @@ -61,7 +61,7 @@ function RvwMnpwBlock({ block, items, selectedPerson }: RvwMnpwBlockProps) {

{block.title}

- {items.length} + {items.length}