From 43fe3a1a99bc65fdc55c618c4e08bb145ebcc02c Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 13 Apr 2026 15:27:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[cs]=20WIP:=20=EC=95=8C=EB=A6=BC=ED=86=A1?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=EC=9D=B4=EB=A0=A5=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ntftalk-history/page.tsx | 3 + apps/cs/docs/spec/ntftalk/history/SPEC.md | 164 ++++++++++++ apps/cs/docs/spec/ntftalk/{ => temp}/SPEC.md | 0 apps/cs/eslint.config.mjs | 2 + .../api/ntftalk-mmt-log-history-service.ts | 58 ++++ apps/cs/src/entities/ntftalk/index.ts | 8 + .../ntftalk-mmt-log-history-interface.ts | 84 ++++++ apps/cs/src/pages/ntftalk-history/index.ts | 1 + .../model/ntftalk-history-table-columns.tsx | 64 +++++ .../ui/ntftalk-history-page.tsx | 247 ++++++++++++++++++ .../ui/ntftalk-temp-detail-dialog.tsx | 7 +- .../ntftalk-temp/ui/ntftalk-temp-page.tsx | 54 ++-- .../ui/ntftalk-temp-regist-dialog.tsx | 45 ++-- apps/cs/src/shared/config/api.ts | 1 + .../src/shared/ui/input-date-range-picker.tsx | 12 +- .../src/ui/input-date-picker/index.tsx | 8 +- 16 files changed, 702 insertions(+), 56 deletions(-) create mode 100644 apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx create mode 100644 apps/cs/docs/spec/ntftalk/history/SPEC.md rename apps/cs/docs/spec/ntftalk/{ => temp}/SPEC.md (100%) create mode 100644 apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts create mode 100644 apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts create mode 100644 apps/cs/src/pages/ntftalk-history/index.ts create mode 100644 apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx create mode 100644 apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx diff --git a/apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx b/apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx new file mode 100644 index 000000000..3e2afeb35 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx @@ -0,0 +1,3 @@ +import { NtftalkHistoryPage } from "@/pages/ntftalk-history"; + +export default NtftalkHistoryPage; diff --git a/apps/cs/docs/spec/ntftalk/history/SPEC.md b/apps/cs/docs/spec/ntftalk/history/SPEC.md new file mode 100644 index 000000000..51740470a --- /dev/null +++ b/apps/cs/docs/spec/ntftalk/history/SPEC.md @@ -0,0 +1,164 @@ +# 화면설계서: 알림톡발송이력관리 + +## 1. 기본 정보 + +| 항목 | 내용 | +| ---------- | -------------------------------------------- | +| 버전 | 1.0 | +| 프로세스명 | UI설계 | +| 화면명 | 알림톡발송이력관리 | +| 화면 유형 | [x] 조회 [ ] 입력 [ ] 삭제 [ ] 수정 [ ] 출력 | +| 메뉴 경로 | home > 시스템관리 > 알림톡발송이력관리 | + +--- + +## 2. 화면 목적 + +알림톡 발송 이력을 **기간·내용·수신자·결과·시스템** 조건으로 검색하고, **발송 시스템·알림 내용·수신자·수신 시각·성공 여부**를 목록으로 확인한다. + +--- + +## 3. 화면 구성 + +### 3-1. 전체 레이아웃 + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ 페이지 제목: 알림톡발송이력관리 브레드크럼 (우측 상단) │ +├────────────────────────────────────────────────────────────────────┤ +│ [영역 A] 검색 필터 │ +│ · 조회기간, 알림내용, 사용자, 결과, 시스템 │ +│ · [조회], [초기화] │ +├────────────────────────────────────────────────────────────────────┤ +│ [영역 B] 알림톡 발송 이력 그리드 │ +│ · 순번, 시스템, 알림내용, 수신자, 수신일, 결과 │ +│ · 페이지네이션 │ +└────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. 영역 A: 검색 필터 + +### 4-1. 필드 정의 + +| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 | +| -------- | -------------------- | ---- | ------------- | ----------------------------------- | ------------------------------------------- | +| 조회기간 | InputDateRangePicker | N | 1개월 전~오늘 | `dateFrom`, `dateTo` (`yyyy-MM-dd`) | `src/shared/ui/input-date-range-picker.tsx` | +| 알림내용 | 텍스트 | N | 공백 | `content` | | +| 사용자 | 텍스트 | N | 공백 | `recipientNum` | | +| 결과 | 드롭다운 | N | 전체 | `successYn` | `SUCCESS` \| `FAIL` \| `ALL` | +| 시스템 | SiteSelect | N | 전체 | `system` | `src/entities/site/ui/site-select.tsx` | + +### 4-2. 버튼 + +| 버튼명 | 동작 | +| ------ | ---------------------------------------------------------------- | +| 조회 | 현재 필터로 영역 B 목록을 재조회한다. 페이지는 1페이지로 초기화. | +| 초기화 | 검색 필드를 기본값으로 되돌린다. | + +--- + +## 5. 영역 B: 알림톡 발송 이력 그리드 + +### 5-1. 그리드 컬럼 + +| 표시명 | Field | 비고 | +| -------- | -------------- | ------------------------------------------------------------------------- | +| 순번 | — | 클라이언트 표시용 인덱스(서버 필드 없음). 페이지네이션 시 오프셋 반영 | +| 시스템 | `siteSystem` | 응답 시 `templateCode` 앞 4자 기반 파생(행정지원·하수도통계·포털·기타) | +| 알림내용 | `content` | 긴 텍스트 말줄임 처리. 툴팁 적용하여 전체 텍스트 확인 가능. | +| 수신자 | `recipientNum` | 실데이터·마스킹 규칙 **TBD** | +| 수신일 | `dateRslt` | `YYYY-MM-DD HH:mm:ss` 형식 표시. | +| 결과 | `successYn` | UI 표시: `SUCCESS` → 성공, `FAIL` → 실패(백엔드는 `reportCode` 기반 파생) | + +- 행 클릭 UX 없음. + +--- + +## 6. API/DTO 명세 + +### 6-1. 공통 + +- 목록 응답: Spring `Page` 직렬화 형태 — 프론트 타입 `PageableList` (`content`, `totalElements`, `totalPages`, `number`, `size` 등). +- 목록 요청 쿼리: `Pageable` 표준 파라미터 `page`, `size`, `sort` (`PageableParams`). + +### 6-2. 알림톡 발송 이력 API + +Base: `/ntftalk-service/api/v1/ata/mmt-log-history` + +| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) | +| ----------------- | ------ | ----------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------- | +| 목록 조회(페이징) | GET | `/ata/mmt-log-history` | Query: `AtaMmtLogHistorySearchDto` 필드 + `Pageable`(`page`,`size`,`sort`) | `Page` → `NtftalkMmtLogHistoryPageList` | `getNtftalkMmtLogHistoryPage` | +| 상세 조회 | GET | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | `AtaMmtLogHistoryResponseDto` → `NtftalkMmtLogHistory` | `getNtftalkMmtLogHistory` | +| 등록 | POST | `/ata/mmt-log-history` | Body: `AtaMmtLogHistorySaveRequestDto` → `NtftalkMmtLogHistorySavePayload` | `AtaMmtLogHistoryResponseDto` | `saveNtftalkMmtLogHistory` | +| 수정 | PUT | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr`, Body: `AtaMmtLogHistoryUpdateRequestDto` → `NtftalkMmtLogHistoryUpdatePayload` | `AtaMmtLogHistoryResponseDto` | `updateNtftalkMmtLogHistory` | +| 삭제 | DELETE | `/ata/mmt-log-history/{mtPr}` | Path: `mtPr` | 없음(204) | `deleteNtftalkMmtLogHistory` | + +#### 검색 Query ↔ TypeScript (`NtftalkMmtLogHistorySearchParams`) + +| Query (Java 필드) | TS 필드 | 설명 | +| ----------------- | --------------- | ------------------------------------ | +| `subject` | `subject?` | 메시지 제목 | +| `content` | `content?` | 전송 메시지 내용 | +| `recipientNum` | `recipientNum?` | 수신자 전화번호 | +| `msgStatus` | `msgStatus?` | `1` 전송대기, `2` 결과대기, `3` 완료 | +| `reportCode` | `reportCode?` | 전송 결과 코드(`1000`: 성공) | +| `dateFrom` | `dateFrom?` | `yyyy-MM-dd` | +| `dateTo` | `dateTo?` | `yyyy-MM-dd` | +| `system` | `system?` | `ADSS` \| `CWMS` \| `PORT` \| `ALL` | +| `successYn` | `successYn?` | `SUCCESS` \| `FAIL` \| `ALL` | + +#### 응답 행 ↔ TypeScript (`NtftalkMmtLogHistory`) + +백엔드 `AtaMmtLogHistoryResponseDto`와 동일 필드명: `mtPr`, `mtRefkey`, `priority`, `dateClientReq`, `subject`, `content`, `callback`, `msgStatus`, `recipientNum`, `dateMtSent`, `dateRslt`, `dateMtReport`, `reportCode`, `countryCode`, `msgType`, `regDate`, `senderKey`, `templateCode`, `siteSystem`, `successYn`. + +--- + +## 7. UI 구현 가이드 + +### 7-1. 기술 스택·원칙 + +- **UI**: `@common/components` 공통 컴포넌트 +- **테이블**: `@tanstack/react-table` + `useDataTable`(`src/shared/lib/use-data-table.tsx`) +- **데이터**: `@tanstack/react-query` — 목록은 `getNtftalkMmtLogHistoryPage`, 무한 스크롤 vs 페이지 UI는 **TBD** +- **레이아웃**: App Router `app/(with-base-layout)/ntftalk-history/page.tsx`에서 페이지 UI 마운트 + +### 7-2. 모듈·파일 배치 + +| 구분 | 경로 | +| --------------- | ------------------------------------------------------------------------------------------------------------------- | +| 라우트 엔트리 | `apps/cs/app/(with-base-layout)/ntftalk-history/page.tsx` | +| 페이지 셸 | `apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx` | +| 컬럼 정의 | `apps/cs/src/pages/ntftalk-history/model/*-table-columns.tsx` | +| API·타입 | `apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts`, `api/ntftalk-mmt-log-history-service.ts` | +| 엔드포인트 상수 | `apps/cs/src/shared/config/api.ts` — `ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY` | + +### 7-3. 영역별 UI 매핑 + +| 영역 | 화면 요소 | 컴포넌트·훅 | 구현 비고 | +| ---- | ------------ | ------------------------------------------------ | ---------------------------- | +| A | 검색 폼 | `Input`, `Label`, `Select`, `Button` | | +| B | 이력 그리드 | `useDataTable` + `ColumnDef`, `manualPagination` | | +| 공통 | 로딩·빈 목록 | `LoadingWithAnimation`, 빈 데이터 안내 | 기타 목록 페이지와 동일 수준 | + +### 7-4. 데이터 흐름 + +``` +BEGIN + + // 최초 진입 + CALL 알림톡발송이력목록조회(기본 검색 조건, page=0, size=10) + DISPLAY 알림톡 발송 이력 그리드 (영역 B) + + ON [조회] 버튼 CLICK DO + CALL 알림톡발송이력목록조회(조회기간→dateFrom/dateTo, content, recipientNum, successYn, system, page=0) + DISPLAY 알림톡 발송 이력 그리드 (영역 B) + END ON + + ON [초기화] 버튼 CLICK DO + RESET 검색 조건 → 기본값 + END ON + +END +``` diff --git a/apps/cs/docs/spec/ntftalk/SPEC.md b/apps/cs/docs/spec/ntftalk/temp/SPEC.md similarity index 100% rename from apps/cs/docs/spec/ntftalk/SPEC.md rename to apps/cs/docs/spec/ntftalk/temp/SPEC.md diff --git a/apps/cs/eslint.config.mjs b/apps/cs/eslint.config.mjs index e11052b52..c2643e6aa 100644 --- a/apps/cs/eslint.config.mjs +++ b/apps/cs/eslint.config.mjs @@ -1,6 +1,7 @@ import { dirname } from "path"; import { fileURLToPath } from "url"; import { FlatCompat } from "@eslint/eslintrc"; +import baseEsLintConfig from "../../eslint.config.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -11,6 +12,7 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + ...baseEsLintConfig, { rules: { "react-hooks/exhaustive-deps": "off", diff --git a/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts b/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts new file mode 100644 index 000000000..ebfd90bdf --- /dev/null +++ b/apps/cs/src/entities/ntftalk/api/ntftalk-mmt-log-history-service.ts @@ -0,0 +1,58 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + type NtftalkMmtLogHistory, + type NtftalkMmtLogHistoryPageList, + type NtftalkMmtLogHistorySavePayload, + type NtftalkMmtLogHistorySearchParams, + type NtftalkMmtLogHistoryService, + type NtftalkMmtLogHistoryUpdatePayload, +} from "../model/ntftalk-mmt-log-history-interface"; + +class NtftalkMmtLogHistoryApiService implements NtftalkMmtLogHistoryService { + async getNtftalkMmtLogHistoryPage( + params: NtftalkMmtLogHistorySearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`, + { params }, + ); + return response.data; + } + + async getNtftalkMmtLogHistory(mtPr: number): Promise { + const response = await client.get( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + ); + return response.data; + } + + async saveNtftalkMmtLogHistory( + payload: NtftalkMmtLogHistorySavePayload, + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}`, + payload, + ); + return response.data; + } + + async updateNtftalkMmtLogHistory( + mtPr: number, + payload: NtftalkMmtLogHistoryUpdatePayload, + ): Promise { + const response = await client.put( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + payload, + ); + return response.data; + } + + async deleteNtftalkMmtLogHistory(mtPr: number): Promise { + await client.delete( + `${ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY}/${mtPr}`, + ); + } +} + +export const NtftalkMmtLogHistoryApi = new NtftalkMmtLogHistoryApiService(); diff --git a/apps/cs/src/entities/ntftalk/index.ts b/apps/cs/src/entities/ntftalk/index.ts index 048eaad9e..0210af8a4 100644 --- a/apps/cs/src/entities/ntftalk/index.ts +++ b/apps/cs/src/entities/ntftalk/index.ts @@ -1,6 +1,7 @@ export { NtftalkVarApi } from "./api/ntftalk-var-service"; export { NtftalkTempApi } from "./api/ntftalk-temp-service"; export { NtftalkBiztalkApi } from "./api/ntftalk-biztalk-service"; +export { NtftalkMmtLogHistoryApi } from "./api/ntftalk-mmt-log-history-service"; export type { NtftalkVar, NtftalkVarPageList, @@ -27,3 +28,10 @@ export type { NtftalkTempSearchParams, NtftalkTempUpdatePayload, } from "./model/ntftalk-temp-interface"; +export type { + NtftalkMmtLogHistory, + NtftalkMmtLogHistoryPageList, + NtftalkMmtLogHistorySavePayload, + NtftalkMmtLogHistorySearchParams, + NtftalkMmtLogHistoryUpdatePayload, +} from "./model/ntftalk-mmt-log-history-interface"; diff --git a/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts b/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts new file mode 100644 index 000000000..9f272d1ac --- /dev/null +++ b/apps/cs/src/entities/ntftalk/model/ntftalk-mmt-log-history-interface.ts @@ -0,0 +1,84 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface NtftalkMmtLogHistorySearchParams extends PageableParams { + subject?: string; + content?: string; + recipientNum?: string; + /** 1:전송대기, 2:결과대기, 3:완료 */ + msgStatus?: string; + /** 1000:성공 */ + reportCode?: string; + /** yyyy-MM-dd */ + dateFrom?: string; + /** yyyy-MM-dd */ + dateTo?: string; + /** ADSS | CWMS | PORT | ALL */ + system?: string; + /** SUCCESS | FAIL | ALL */ + successYn?: string; +} + +export interface NtftalkMmtLogHistory { + mtPr: number; + mtRefkey?: string | null; + priority?: string | null; + dateClientReq?: string | null; + subject?: string | null; + content?: string | null; + callback?: string | null; + msgStatus?: string | null; + recipientNum?: string | null; + dateMtSent?: string | null; + dateRslt?: string | null; + dateMtReport?: string | null; + reportCode?: string | null; + countryCode?: string | null; + msgType?: number | null; + regDate?: string | null; + senderKey?: string | null; + templateCode?: string | null; + siteSystem?: string | null; + successYn?: string | null; +} + +export interface NtftalkMmtLogHistorySavePayload { + mtPr: number; + mtRefkey?: string; + priority?: string; + dateClientReq?: string; + subject: string; + content: string; + callback: string; + msgStatus?: string; + recipientNum: string; + reportCode?: string; + countryCode?: string; + msgType?: number; + senderKey: string; + templateCode?: string; + responseMethod?: string; +} + +export interface NtftalkMmtLogHistoryUpdatePayload { + content: string; + recipientNum: string; + msgStatus?: string; + reportCode?: string; +} + +export type NtftalkMmtLogHistoryPageList = PageableList; + +export interface NtftalkMmtLogHistoryService { + getNtftalkMmtLogHistoryPage( + params: NtftalkMmtLogHistorySearchParams, + ): Promise; + getNtftalkMmtLogHistory(mtPr: number): Promise; + saveNtftalkMmtLogHistory( + payload: NtftalkMmtLogHistorySavePayload, + ): Promise; + updateNtftalkMmtLogHistory( + mtPr: number, + payload: NtftalkMmtLogHistoryUpdatePayload, + ): Promise; + deleteNtftalkMmtLogHistory(mtPr: number): Promise; +} diff --git a/apps/cs/src/pages/ntftalk-history/index.ts b/apps/cs/src/pages/ntftalk-history/index.ts new file mode 100644 index 000000000..e9871422b --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/index.ts @@ -0,0 +1 @@ +export { NtftalkHistoryPage } from "./ui/ntftalk-history-page"; diff --git a/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx b/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx new file mode 100644 index 000000000..637285075 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/model/ntftalk-history-table-columns.tsx @@ -0,0 +1,64 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { NtftalkMmtLogHistory } from "@/entities/ntftalk"; + +function formatDateTime(value: string | null | undefined): string { + if (!value) return "—"; + const normalized = value.includes("T") + ? value.replace("T", " ").slice(0, 19) + : value.slice(0, 19); + return normalized; +} + +function formatResult(successYn: string | null | undefined): string { + if (!successYn) return "—"; + if (successYn === "SUCCESS") return "성공"; + if (successYn === "FAIL") return "실패"; + return successYn; +} + +export function createNtftalkHistoryColumns(): ColumnDef[] { + return [ + { + id: "rowNo", + header: "순번", + enableSorting: false, + cell: ({ row, table }) => { + const { pageIndex, pageSize } = table.getState().pagination; + return pageIndex * pageSize + row.index + 1; + }, + }, + { + accessorKey: "siteSystem", + header: "시스템", + cell: ({ getValue }) => (getValue() as string) || "—", + }, + { + accessorKey: "content", + header: "알림내용", + cell: ({ getValue }) => { + const v = getValue() as string; + if (!v) return "—"; + return ( + + {v} + + ); + }, + }, + { + accessorKey: "recipientNum", + header: "수신자", + cell: ({ getValue }) => (getValue() as string) || "—", + }, + { + accessorKey: "dateRslt", + header: "수신일", + cell: ({ getValue }) => formatDateTime(getValue() as string | null), + }, + { + accessorKey: "successYn", + header: "결과", + cell: ({ getValue }) => formatResult(getValue() as string | null), + }, + ]; +} diff --git a/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx new file mode 100644 index 000000000..1f2e0e9c4 --- /dev/null +++ b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useMemo, useState } from "react"; + +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; +import dayjs from "dayjs"; + +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; +import { + NtftalkMmtLogHistoryApi, + type NtftalkMmtLogHistorySearchParams, +} from "@/entities/ntftalk"; +import { SiteSelect } from "@/entities/site"; +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +import { createNtftalkHistoryColumns } from "../model/ntftalk-history-table-columns"; + +type DateRange = { + from?: Date | null; + to?: Date | null; +}; + +type PresetPeriod = "" | "3M" | "6M" | "1Y" | "CUSTOM"; + +function computeDateRangeFromPreset( + preset: Extract, +): DateRange { + const to = dayjs().endOf("day").toDate(); + const base = dayjs(); + let fromDay: dayjs.Dayjs; + switch (preset) { + case "3M": + fromDay = base.subtract(3, "month"); + break; + case "6M": + fromDay = base.subtract(6, "month"); + break; + case "1Y": + fromDay = base.subtract(1, "year"); + break; + } + return { from: fromDay.startOf("day").toDate(), to }; +} + +function NtftalkHistoryPage() { + const [contentInput, setContentInput] = useState(""); + const [recipientInput, setRecipientInput] = useState(""); + const [presetPeriod, setPresetPeriod] = useState(""); + const [dateRange, setDateRange] = useState({ + from: null, + to: null, + }); + const [resultFilter, setResultFilter] = useState("ALL"); + const [systemFilter, setSystemFilter] = useState("ALL"); + + const [searchParams, setSearchParams] = + useState({ sort: "dateMtReport,desc" }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const listQuery = useQuery({ + queryKey: [ + ENDPOINT.NTFTALK_SERVICE.MMT_LOG_HISTORY, + searchParams, + pagination, + ], + queryFn: () => + NtftalkMmtLogHistoryApi.getNtftalkMmtLogHistoryPage({ + ...searchParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const columns = useMemo(() => createNtftalkHistoryColumns(), []); + + const { tableComponent } = useDataTable({ + query: listQuery, + tableOptions: { + columns, + state: { pagination }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const buildSearchParamsFromInputs = (): NtftalkMmtLogHistorySearchParams => ({ + sort: searchParams.sort ?? "dateMtReport,desc", + content: contentInput.trim() || undefined, + recipientNum: recipientInput.trim() || undefined, + successYn: resultFilter, + system: systemFilter, + dateFrom: dateRange.from + ? dayjs(dateRange.from).format("YYYY-MM-DD") + : undefined, + dateTo: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : undefined, + }); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setPagination((p) => ({ ...p, pageIndex: 0 })); + setSearchParams(buildSearchParamsFromInputs()); + }; + + const handlePresetPeriodChange = (value: PresetPeriod) => { + setPresetPeriod(value); + if (value === "3M" || value === "6M" || value === "1Y") { + setDateRange(computeDateRangeFromPreset(value)); + } + }; + + const handleDateRangeChange = (next: DateRange) => { + setDateRange(next); + setPresetPeriod("CUSTOM"); + }; + + const handleReset = () => { + setContentInput(""); + setRecipientInput(""); + setPresetPeriod(""); + setDateRange({ from: null, to: null }); + setResultFilter("ALL"); + setSystemFilter("ALL"); + setPagination((p) => ({ ...p, pageIndex: 0 })); + setSearchParams({ sort: "dateMtReport,desc" }); + }; + + return ( +
+
+
+
+
+
+ + + +
+
+
+
+ + setContentInput(e.target.value)} + /> +
+
+ + setRecipientInput(e.target.value)} + placeholder="수신자 번호" + /> +
+
+ + +
+
+ + setSystemFilter(value)} + enabledAllSite + /> +
+
+
+
+ + +
+
+
+ +
+
+

알림톡 발송 이력

+
+
{tableComponent}
+
+
+ ); +} + +export { NtftalkHistoryPage }; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx index f196df4e3..aa08f92ca 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-detail-dialog.tsx @@ -1,6 +1,10 @@ "use client"; import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; +import { NoData } from "@/shared/ui/no-data"; +import { NtftalkTempApi } from "@/entities/ntftalk"; import { Dialog, DialogContent, @@ -13,9 +17,6 @@ import { TableHead, TableRow, } from "@common/components"; -import { NtftalkTempApi } from "@/entities/ntftalk"; -import { ENDPOINT } from "@/shared/config"; -import { NoData } from "@/shared/ui/no-data"; interface NtftalkTempDetailDialogProps { templateCode: string | null; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx index 8cdd4e338..57db5d6e2 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx @@ -2,12 +2,38 @@ import { useCallback, useMemo, useState } from "react"; import { useFieldArray, useForm, useWatch } from "react-hook-form"; + import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { keepPreviousData, useQuery, useQueryClient, } from "@tanstack/react-query"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { z } from "zod"; + +import { AUTH_USER_ID, ENDPOINT } from "@/shared/config"; +import { + getCookie, + useAlertStore, + useDataTable, + useMutationWrapper, +} from "@/shared/lib"; +import { NoData } from "@/shared/ui/no-data"; +import { + NtftalkBiztalkApi, + type NtftalkBiztalkInspectionRequestItem, + type NtftalkTemp, + NtftalkTempApi, + type NtftalkTempSearchParams, + NtftalkVarApi, + type NtftalkVarSearchParams, +} from "@/entities/ntftalk"; import { Button, DataTable, @@ -20,36 +46,14 @@ import { SelectTrigger, SelectValue, } from "@common/components"; -import { - getCoreRowModel, - type PaginationState, - type RowSelectionState, - useReactTable, -} from "@tanstack/react-table"; -import { z } from "zod"; -import { - NtftalkBiztalkApi, - NtftalkTempApi, - NtftalkVarApi, - type NtftalkBiztalkInspectionRequestItem, - type NtftalkTemp, - type NtftalkTempSearchParams, - type NtftalkVarSearchParams, -} from "@/entities/ntftalk"; -import { AUTH_USER_ID, ENDPOINT } from "@/shared/config"; -import { - getCookie, - useAlertStore, - useDataTable, - useMutationWrapper, -} from "@/shared/lib"; -import { NoData } from "@/shared/ui/no-data"; -import { createTemplateColumns } from "../model/template-table-columns"; + import { createEmptyVarDraft, type NtftalkVarGridRow, } from "../model/ntftalk-var-grid"; +import { createTemplateColumns } from "../model/template-table-columns"; import { createVarColumns } from "../model/var-table-columns"; + import { NtftalkTempDetailDialog } from "./ntftalk-temp-detail-dialog"; import { NtftalkTempRegistDialog } from "./ntftalk-temp-regist-dialog"; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx index 6a8937073..8a537e7a2 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-regist-dialog.tsx @@ -1,8 +1,9 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; import { useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; import { keepPreviousData, useQuery, @@ -13,10 +14,28 @@ import { type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; + +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/shared/ui/form"; +import { NoData } from "@/shared/ui/no-data"; +import { + NtftalkBiztalkApi, + NtftalkTempApi, + NtftalkVarApi, + type NtftalkVarSearchParams, +} from "@/entities/ntftalk"; import { Button, Dialog, DialogContent, + DialogFooter, DialogHeader, DialogTitle, Input, @@ -33,29 +52,13 @@ import { TableBody, TableCell, TableHead, - Textarea, TableRow, - DialogFooter, + Textarea, } from "@common/components"; + import { - NtftalkBiztalkApi, - NtftalkTempApi, - NtftalkVarApi, - type NtftalkVarSearchParams, -} from "@/entities/ntftalk"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib"; -import { NoData } from "@/shared/ui/no-data"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/shared/ui/form"; -import { - ntftalkTempSaveFormSchema, type NtftalkTempSaveFormData, + ntftalkTempSaveFormSchema, } from "../model/ntftalk-temp-save-schema"; import { createVarColumns } from "../model/var-table-columns"; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index c380a8817..e577c5bb1 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -70,6 +70,7 @@ export const ENDPOINT = { NTFTALK_VAR: "/ntftalk-service/api/v1/ntftalk-var", NTFTALK_TEMP: "/ntftalk-service/api/v1/ntftalk-temp", NTFTALK_BIZTALK: "/ntftalk-service/api/v1/ntftalk-biztalk", + MMT_LOG_HISTORY: "/ntftalk-service/api/v1/ata/mmt-log-history", }, }; diff --git a/apps/cs/src/shared/ui/input-date-range-picker.tsx b/apps/cs/src/shared/ui/input-date-range-picker.tsx index 1ee2bc5e1..ec7c5b5b4 100644 --- a/apps/cs/src/shared/ui/input-date-range-picker.tsx +++ b/apps/cs/src/shared/ui/input-date-range-picker.tsx @@ -1,20 +1,24 @@ -import { InputDatePicker } from "@common/components"; import { useEffect } from "react"; + +import { InputDatePicker } from "@common/components"; + import { useAlertStore } from "../lib"; type DateRange = { - from?: Date; - to?: Date; + from?: Date | null; + to?: Date | null; }; type InputDateRangePickerProps = { dateRange: DateRange; setDateRange: (dateRange: DateRange) => void; + disabled?: boolean; }; function InputDateRangePicker({ dateRange, setDateRange, + disabled, }: InputDateRangePickerProps) { const alert = useAlertStore((state) => state.setMessage); @@ -32,6 +36,7 @@ function InputDateRangePicker({ onChange={(date) => { setDateRange({ ...dateRange, from: date }); }} + disabled={disabled} /> ~ { setDateRange({ ...dateRange, to: date }); }} + disabled={disabled} /> ); diff --git a/packages/common/components/src/ui/input-date-picker/index.tsx b/packages/common/components/src/ui/input-date-picker/index.tsx index 93d2caf7c..e5ec1eac3 100644 --- a/packages/common/components/src/ui/input-date-picker/index.tsx +++ b/packages/common/components/src/ui/input-date-picker/index.tsx @@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "../popover"; import "./input-date-picker.css"; export interface InputDatePickerProps { - value?: Date; + value?: Date | null; defaultValue?: Date; onChange?: (date: Date | undefined) => void; placeholder?: string; @@ -34,7 +34,7 @@ export function InputDatePicker({ const [internalDate, setInternalDate] = useState( defaultValue, ); - const [month, setMonth] = useState( + const [month, setMonth] = useState( isControlled ? controlledValue : defaultValue, ); @@ -112,9 +112,9 @@ export function InputDatePicker({ > { handleDateChange(newDate); From 68f77151bf3e163a1300f7a99104abf2c6a9a5ed Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 13 Apr 2026 15:43:19 +0900 Subject: [PATCH 2/2] chore: FormEvent -> SubmitEvent --- apps/_template/features/README.md | 5 +- .../src/pages/auth/login/ui/SignupModal.tsx | 230 ++- .../pages/auth/login/ui/UserSignupModal.tsx | 2 +- .../cm/src/pages/auth/login/ui/login-page.tsx | 81 +- apps/cm/src/pages/users/ui/users-page.tsx | 2 +- .../src/pages/(post)/faq/ui/faq-page.tsx | 2 +- .../src/pages/auth/login/ui/login-page.tsx | 2 +- .../src/pages/search/ui/search-page.tsx | 5 +- .../src/widgets/post/ui/post-page.tsx | 2 +- apps/cs/src/pages/(post)/faq/ui/faq-page.tsx | 2 +- .../src/pages/admin/ui/add-admin-dialog.tsx | 2 +- apps/cs/src/pages/admin/ui/admin-page.tsx | 2 +- apps/cs/src/pages/apilog/ui/apilog-page.tsx | 2 +- .../cs/src/pages/auth/login/ui/login-page.tsx | 2 +- apps/cs/src/pages/banner/ui/banner-page.tsx | 2 +- apps/cs/src/pages/bbs/ui/bbs-page.tsx | 2 +- .../ui/board-statistics-page.tsx | 2 +- .../cs/src/pages/code/ui/lwpocode-section.tsx | 2 +- apps/cs/src/pages/code/ui/upcode-section.tsx | 2 +- .../src/pages/contents/ui/contents-page.tsx | 2 +- apps/cs/src/pages/file/ui/file-page.tsx | 2 +- .../src/pages/groups/ui/add-user-dialog.tsx | 2 +- .../pages/groups/ui/group-users-section.tsx | 50 +- .../cs/src/pages/groups/ui/groups-section.tsx | 86 +- .../pages/groups/ui/menu-authority-dialog.tsx | 2 +- apps/cs/src/pages/image/ui/image-page.tsx | 2 +- apps/cs/src/pages/link/ui/link-page.tsx | 292 ++-- .../cs/src/pages/linkLog/ui/link-log-page.tsx | 371 +++-- .../pages/linkParams/ui/link-params-page.tsx | 741 ++++----- .../pages/linkStats/ui/link-stats-page.tsx | 737 +++++---- .../pages/linkTable/ui/link-table-page.tsx | 1425 +++++++++-------- .../src/pages/loginlog/ui/loginlog-page.tsx | 2 +- .../src/pages/menu/ui/menu-history-dialog.tsx | 2 +- .../menu/ui/organization-search-dialog.tsx | 2 +- apps/cs/src/pages/module/ui/module-page.tsx | 2 +- .../ui/ntftalk-history-page.tsx | 15 +- .../ntftalk-temp/ui/ntftalk-temp-page.tsx | 2 +- apps/cs/src/pages/popup/ui/popup-page.tsx | 2 +- .../pages/privacylog/ui/privacylog-page.tsx | 2 +- apps/cs/src/pages/recent/ui/recent-page.tsx | 2 +- .../src/pages/role/ui/mapping-role-dialog.tsx | 2 +- apps/cs/src/pages/role/ui/role-page.tsx | 2 +- apps/cs/src/pages/site/ui/site-page.tsx | 2 +- apps/cs/src/pages/survey/ui/survey-page.tsx | 2 +- .../src/pages/template/ui/template-page.tsx | 2 +- apps/cs/src/pages/terms/ui/terms-page.tsx | 2 +- .../ui/user-statistics-page.tsx | 2 +- apps/cs/src/pages/users/ui/users-page.tsx | 2 +- apps/cs/src/widgets/post/ui/post-page.tsx | 2 +- .../dc/src/pages/auth/login/ui/login-page.tsx | 2 +- .../src/pages/category/ui/category-page.tsx | 284 ++-- apps/dc/src/pages/chatbot/ui/chatbot-page.tsx | 144 +- .../(landing)/reuse/_components/TopNav.tsx | 48 +- apps/dm/components/LoginModal.tsx | 40 +- apps/st/src/components/module/SearchForm.tsx | 194 ++- .../module/SidoSggFacilityNameSearchForm.tsx | 215 ++- .../statistics/management/LogHistory.tsx | 110 +- .../VerificationArticleManagement.tsx | 2 +- .../st/src/pages/auth/login/ui/login-page.tsx | 234 +-- apps/wr/components/lib/func/TableData.tsx | 113 +- .../features/auth/login/ui/SignupModal.tsx | 230 ++- .../auth/login/ui/UserSignupModal.tsx | 2 +- .../src/features/auth/login/ui/login-page.tsx | 7 +- 63 files changed, 3227 insertions(+), 2510 deletions(-) diff --git a/apps/_template/features/README.md b/apps/_template/features/README.md index 2be9a3e17..329175c45 100644 --- a/apps/_template/features/README.md +++ b/apps/_template/features/README.md @@ -13,6 +13,7 @@ > ⚠️ **중요**: 모든 것을 feature로 만들 필요는 없습니다. Feature로 만드는 것이 좋은 지표: + - **여러 페이지에서 재사용됨**: 예를 들어, 여러 에디터가 있고 모두 댓글 기능이 있다면, 댓글은 재사용 가능한 feature입니다 - **중요한 비즈니스 기능**: 신규 프로젝트 참여자가 Features 폴더를 보고 앱의 주요 기능을 빠르게 파악할 수 있어야 합니다 @@ -56,7 +57,7 @@ export function LoginForm() { password: '' }); - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); await loginUser(credentials); }; @@ -104,11 +105,13 @@ export type { LoginCredentials } from './model/types'; ## 레이어 의존성 규칙 ✅ **허용되는 의존성**: + - `shared` 레이어의 모듈 - `entities` 레이어의 모듈 - 같은 feature 슬라이스 내의 다른 세그먼트 ❌ **금지된 의존성**: + - 다른 `features` 슬라이스 (feature 간 직접 의존 금지) - `widgets` 레이어 - `pages` 레이어 diff --git a/apps/cm/src/pages/auth/login/ui/SignupModal.tsx b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx index d61949ccc..11d7d3544 100644 --- a/apps/cm/src/pages/auth/login/ui/SignupModal.tsx +++ b/apps/cm/src/pages/auth/login/ui/SignupModal.tsx @@ -14,7 +14,9 @@ interface SignupModalProps { } function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement'); + const [currentStep, setCurrentStep] = useState<"agreement" | "signup">( + "agreement", + ); const [agreed, setAgreed] = useState(false); const [formData, setFormData] = useState({ userId: "", @@ -47,7 +49,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { // 모달이 닫힐 때 초기화 useEffect(() => { if (!open) { - setCurrentStep('agreement'); + setCurrentStep("agreement"); setAgreed(false); setFormData({ userId: "", @@ -81,17 +83,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const handleNextStep = () => { if (agreed) { - setCurrentStep('signup'); + setCurrentStep("signup"); } }; - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = ( + e: React.ChangeEvent, + ) => { const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - + setFormData((prev) => ({ ...prev, [name]: value })); + // 에러 초기화 if (errors[name]) { - setErrors(prev => ({ ...prev, [name]: "" })); + setErrors((prev) => ({ ...prev, [name]: "" })); } }; @@ -102,20 +106,27 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { if (!formData.userId) newErrors.userId = "아이디를 입력해주세요."; if (!formData.userName) newErrors.userName = "이름을 입력해주세요."; if (!formData.password) newErrors.password = "비밀번호를 입력해주세요."; - if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; + if (!formData.confirmPassword) + newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요."; if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요."; if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요."; // 비밀번호 확인 - if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) { + if ( + formData.password && + formData.confirmPassword && + formData.password !== formData.confirmPassword + ) { newErrors.confirmPassword = "비밀번호가 일치하지 않습니다."; } // 비밀번호 정규식 검증 - const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; + const passwordRegex = + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; if (formData.password && !passwordRegex.test(formData.password)) { - newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; + newErrors.password = + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; } // 이메일 형식 검증 @@ -124,8 +135,10 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { } // 중복 체크 검증 - if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다."; - if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다."; + if (idAvailable === false) + newErrors.userId = "이미 사용 중인 아이디입니다."; + if (emailAvailable === false) + newErrors.email = "이미 사용 중인 이메일입니다."; setErrors(newErrors); const isValid = Object.keys(newErrors).length === 0; @@ -135,7 +148,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const checkUserIdDuplicate = async () => { if (!formData.userId) { - setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." })); + setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." })); return; } @@ -147,19 +160,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, - { userId: formData.userId } + { userId: formData.userId }, ); const exists = response.data; setIdAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." })); + setErrors((prev) => ({ + ...prev, + userId: "이미 사용 중인 아이디입니다.", + })); } else { - setErrors(prev => ({ ...prev, userId: "" })); + setErrors((prev) => ({ ...prev, userId: "" })); } } catch (error) { console.error("아이디 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + userId: "아이디 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingId(false); } @@ -167,7 +186,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const checkEmailDuplicate = async () => { if (!formData.email) { - setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." })); + setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." })); return; } @@ -179,28 +198,34 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/exists`, - { email: formData.email } + { email: formData.email }, ); const exists = response.data; setEmailAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." })); + setErrors((prev) => ({ + ...prev, + email: "이미 사용 중인 이메일입니다.", + })); } else { - setErrors(prev => ({ ...prev, email: "" })); + setErrors((prev) => ({ ...prev, email: "" })); } } catch (error) { console.error("이메일 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + email: "이메일 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingEmail(false); } }; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); console.log("회원가입 버튼 클릭됨"); - + if (!validateForm()) { console.log("폼 검증 실패"); return; @@ -230,19 +255,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { // SignupClient를 통해 익명사용자 회원가입 API 호출 const signupClient = SignupClient.getInstance({ - baseURL: BASE_URL + baseURL: BASE_URL, }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`, - signupData + signupData, ); console.log("회원가입 성공:", response.data); - + // 성공 시 모달 닫기 및 콜백 호출 onSuccess(); onClose(); - + // 폼 초기화 setFormData({ userId: "", @@ -267,13 +292,12 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { setErrors({}); setIdAvailable(null); setEmailAvailable(null); - } catch (error: unknown) { console.error("회원가입 오류:", error); if (error instanceof Error) { - setErrors(prev => ({ ...prev, submit: error.message })); + setErrors((prev) => ({ ...prev, submit: error.message })); } else { - setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." })); + setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." })); } } finally { setIsLoading(false); @@ -286,36 +310,52 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
e.stopPropagation()}>
-

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

- +

+ {currentStep === "agreement" ? "개인정보제공동의" : "회원가입"} +

+
- - {currentStep === 'agreement' ? ( + + {currentStep === "agreement" ? (

개인정보 수집 및 이용 동의

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

- +

1. 수집하는 개인정보 항목

    -
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • -
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
  • + 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, + 기관구분 +
  • +
  • + 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, + 상세기관코드, 사업자등록번호 +
- +

2. 개인정보의 수집 및 이용목적

  • 회원가입 및 관리
  • 서비스 제공 및 운영
  • 고객상담 및 문의응답
- +

3. 개인정보의 보유 및 이용기간

-

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

- +

+ 회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 + 기간까지) +

+

4. 동의 거부권 및 거부 시 불이익

-

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+

+ 개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 + 시 회원가입이 제한됩니다. +

- +
- +
-
- {idAvailable === true && 사용 가능한 아이디입니다.} - {errors.userId && {errors.userId}} + {idAvailable === true && ( + + 사용 가능한 아이디입니다. + + )} + {errors.userId && ( + {errors.userId} + )}
@@ -390,7 +432,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { placeholder="이름을 입력하세요" disabled={isLoading} /> - {errors.userName && {errors.userName}} + {errors.userName && ( + {errors.userName} + )}
@@ -416,8 +460,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { {isCheckingEmail ? "확인중..." : "중복확인"} - {emailAvailable === true && 사용 가능한 이메일입니다.} - {errors.email && {errors.email}} + {emailAvailable === true && ( + + 사용 가능한 이메일입니다. + + )} + {errors.email && ( + {errors.email} + )} @@ -434,7 +484,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.password && {errors.password}} + {errors.password && ( + {errors.password} + )} @@ -451,7 +503,11 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.confirmPassword && {errors.confirmPassword}} + {errors.confirmPassword && ( + + {errors.confirmPassword} + + )} @@ -468,7 +524,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.pswdHnt && {errors.pswdHnt}} + {errors.pswdHnt && ( + {errors.pswdHnt} + )} @@ -485,14 +543,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={100} disabled={isLoading} /> - {errors.pswdAns && {errors.pswdAns}} + {errors.pswdAns && ( + {errors.pswdAns} + )}

연락처 정보

- +
@@ -506,7 +566,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.mphn && {errors.mphn}} + {errors.mphn && ( + {errors.mphn} + )}
@@ -523,14 +585,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.telno && {errors.telno}} + {errors.telno && ( + {errors.telno} + )}

주소 정보

- +
@@ -544,7 +608,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={5} disabled={isLoading} /> - {errors.zip && {errors.zip}} + {errors.zip && ( + {errors.zip} + )}
@@ -561,7 +627,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.addr && {errors.addr}} + {errors.addr && ( + {errors.addr} + )}
@@ -578,14 +646,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.daddr && {errors.daddr}} + {errors.daddr && ( + {errors.daddr} + )}

기관 정보

- +
@@ -601,7 +671,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.laeSeCd && {errors.laeSeCd}} + {errors.laeSeCd && ( + {errors.laeSeCd} + )}
@@ -621,7 +693,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.ogdpSeCd && {errors.ogdpSeCd}} + {errors.ogdpSeCd && ( + {errors.ogdpSeCd} + )}
@@ -638,7 +712,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.instSeCd && {errors.instSeCd}} + {errors.instSeCd && ( + {errors.instSeCd} + )} @@ -655,21 +731,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.dtlInstCd && {errors.dtlInstCd}} + {errors.dtlInstCd && ( + {errors.dtlInstCd} + )} {errors.submit && (
-
{errors.submit}
+
+ {errors.submit} +
)}
- {error && ( -
+
{error}
)} {successMessage && ( -
+
{successMessage}
)} -
- 계정이 없으신가요? +
+ + 계정이 없으신가요?{" "} + - - +
+ + setUserNm(e.target.value)} + /> + + +
diff --git a/apps/cs/src/pages/groups/ui/groups-section.tsx b/apps/cs/src/pages/groups/ui/groups-section.tsx index 9c7f5b6ca..019a4a17f 100644 --- a/apps/cs/src/pages/groups/ui/groups-section.tsx +++ b/apps/cs/src/pages/groups/ui/groups-section.tsx @@ -8,13 +8,13 @@ import { } from "@tanstack/react-query"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { Button, Input, Label } from "@common/components"; -import { GroupApi , type GroupSearchParams} from "@/entities/group"; +import { GroupApi, type GroupSearchParams } from "@/entities/group"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; import { createColumns } from "../model/groups-table-columns"; import { GroupsDialog } from "./groups-dialog"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; type GroupsSectionProps = { handleSelectedGroup: (groupId: string) => void; @@ -29,14 +29,12 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) { const [groupNm, setGroupNm] = useState(""); const [searchParams, setSearchParams] = useState({ groupNm: "", - delYn :"N", - sort : "groupId,desc", + delYn: "N", + sort: "groupId,desc", }); - - const query = useQuery({ - queryKey: [ENDPOINT.CMS_SERVICE.GROUPS,searchParams, pagination], + queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, searchParams, pagination], queryFn: () => GroupApi.findPageGroup({ ...searchParams, @@ -64,7 +62,7 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) { } }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, @@ -81,7 +79,7 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) { setSearchParams({ ...searchParams, groupNm: "", - delYn:"N" + delYn: "N", }); }; const handleRowClick = (row: { groupId: string }) => { @@ -109,41 +107,41 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) { return (
-
- - setGroupNm(e.target.value)} - /> - - - - - setSearchParams({ - ...searchParams, - delYn: e.target.checked ? "Y" : "N", - }) - } - /> -
+
+ + setGroupNm(e.target.value)} + /> + + + + + setSearchParams({ + ...searchParams, + delYn: e.target.checked ? "Y" : "N", + }) + } + /> +
diff --git a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx index cde3cf641..02ca5739f 100644 --- a/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/menu-authority-dialog.tsx @@ -95,7 +95,7 @@ function MenuAuthorityDialog({ groupId }: MenuAuthorityDialogProps) { } }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const keyword = formData.get("keyword") as string; diff --git a/apps/cs/src/pages/image/ui/image-page.tsx b/apps/cs/src/pages/image/ui/image-page.tsx index 6208f180b..212726c5d 100644 --- a/apps/cs/src/pages/image/ui/image-page.tsx +++ b/apps/cs/src/pages/image/ui/image-page.tsx @@ -46,7 +46,7 @@ function ImagePage() { }), }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/link/ui/link-page.tsx b/apps/cs/src/pages/link/ui/link-page.tsx index c1b1052fe..a59b9e751 100644 --- a/apps/cs/src/pages/link/ui/link-page.tsx +++ b/apps/cs/src/pages/link/ui/link-page.tsx @@ -4,7 +4,17 @@ import "@common/assets/styles/grid.css"; import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Button, Input, Label, LoadingWithAnimation, Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@common/components"; +import { + Button, + Input, + Label, + LoadingWithAnimation, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from "@common/components"; import { getCoreRowModel } from "@tanstack/react-table"; import { LinkApi, type Link, type LinkSearchParam } from "@/entities/link"; import { useQuery } from "@tanstack/react-query"; @@ -14,147 +24,161 @@ import { useDataTable } from "@/shared/lib"; import LinkExpiredAlert from "./LinkExpiredAlert"; function LinkPage() { - const router = useRouter(); - const queryClient = useQueryClient(); + const router = useRouter(); + const queryClient = useQueryClient(); - const [linkSn, setLinkSn] = useState(''); - const [linkNm, setLinkNm] = useState(''); - const [linkClfsCd, setLinkClfsCd] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); + const [linkClfsCd, setLinkClfsCd] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '', - linkClfsCd: '', + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + linkClfsCd: "", + }); + + const query = useQuery({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, + linkSearchParam, + ], + queryFn: () => LinkApi.getLinkList(linkSearchParam), + }); + + const handleResetSearchParam = () => { + setLinkSn(""); + setLinkNm(""); + setLinkClfsCd("ALL"); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + linkClfsCd: "ALL", }); + }; - const query = useQuery({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam], - queryFn: () => LinkApi.getLinkList(linkSearchParam), + const handleSubmit = async (e: React.SubmitEvent) => { + e.preventDefault(); + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, + linkClfsCd, }); + }; - const handleResetSearchParam = () => { - setLinkSn(''); - setLinkNm(''); - setLinkClfsCd('ALL'); - setLinkSearchParam({ - ...linkSearchParam, - linkSn: '', - linkNm: '', - linkClfsCd: 'ALL', - }); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLinkSearchParam({ - ...linkSearchParam, - linkSn, - linkNm, - linkClfsCd, - }); - }; - - const handleRowClick = (link: Link) => { - queryClient.removeQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`], - }); - queryClient.removeQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`], - }); - router.push(`link/register/${link.link_sn}`); - }; - - const handleClickRegist = () => { - router.push('link/register'); - }; - - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - }, - dataTableOptions: { - onRowClick: handleRowClick, - viewPagination: true, - }, + const handleRowClick = (link: Link) => { + queryClient.removeQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, + ], }); + queryClient.removeQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkInfo`, + ], + }); + router.push(`link/register/${link.link_sn}`); + }; - return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - - - -
-
-
-
-

연계 목록

- -
- - -
-
-
- {!query.isLoading ? -
{tableComponent}
- : -
-
- -
-
- } -
-
+ const handleClickRegist = () => { + router.push("link/register"); + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + + + +
+
+
+
+

연계 목록

+ +
+ + +
- ); +
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+ ); } -export { LinkPage }; \ No newline at end of file +export { LinkPage }; diff --git a/apps/cs/src/pages/linkLog/ui/link-log-page.tsx b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx index dd39a4726..5a0637b72 100644 --- a/apps/cs/src/pages/linkLog/ui/link-log-page.tsx +++ b/apps/cs/src/pages/linkLog/ui/link-log-page.tsx @@ -3,10 +3,25 @@ import "@common/assets/styles/grid.css"; import { useState } from "react"; -import { Button, Input, Label, DatePicker, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, LoadingWithAnimation } from "@common/components"; +import { + Button, + Input, + Label, + DatePicker, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + LoadingWithAnimation, +} from "@common/components"; import { useQuery } from "@tanstack/react-query"; -import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; import { LinkApi, type LinkSearchParam } from "@/entities/link"; import { columns } from "../model/table-columns"; import dayjs from "dayjs"; @@ -14,176 +29,200 @@ import { useAlertStore } from "@/shared/lib/use-alert-store"; import { useDataTable } from "@/shared/lib"; function LinkLogPage() { - const { setMessage: alert } = useAlertStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); - const [linkSn, setLinkSn] = useState(''); - const [linkNm, setLinkNm] = useState(''); - const [isSucc, setIsSucc] = useState('ALL'); - const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); + const [isSucc, setIsSucc] = useState("ALL"); + const [startDate, setStartDate] = useState( + dayjs().subtract(7, "day").format("YYYY-MM-DD"), + ); + const [endDate, setEndDate] = useState(dayjs().format("YYYY-MM-DD")); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '', - isSucc: '', - startDate: '', - endDate: '', + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + isSucc: "", + startDate: "", + endDate: "", + }); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const [rowSelection, setRowSelection] = useState({}); + + const query = useQuery({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, + linkSearchParam, + pagination, + ], + queryFn: () => + LinkApi.getLinkLogList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + isSucc, + startDate, + endDate, + }), + }); + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleResetSearchParam = () => { + setIsSucc("ALL"); + setLinkNm(""); + setLinkSn(""); + setStartDate(dayjs().subtract(7, "day").format("YYYY-MM-DD")); + setEndDate(dayjs().format("YYYY-MM-DD")); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + isSucc: "ALL", + startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"), + endDate: dayjs().format("YYYY-MM-DD"), }); + }; - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, - }); + const handleSubmit = async (e?: React.SubmitEvent) => { + e?.preventDefault(); - const [rowSelection, setRowSelection] = useState({}); - - const query = useQuery({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, linkSearchParam, pagination], - queryFn: () => - LinkApi.getLinkLogList({ - ...linkSearchParam, - page: pagination.pageIndex, - size: pagination.pageSize, - isSucc, - startDate, - endDate, - }), - }); - - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - rowSelection, - }, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - }, - dataTableOptions: { - viewPagination: true, - }, - }); - - const handleResetSearchParam = () => { - setIsSucc('ALL'); - setLinkNm(''); - setLinkSn(''); - setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - setEndDate(dayjs().format('YYYY-MM-DD')); - setLinkSearchParam({ - ...linkSearchParam, - linkSn: '', - linkNm: '', - isSucc: 'ALL', - startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), - endDate: dayjs().format('YYYY-MM-DD'), + if (startDate && endDate) { + if (startDate > endDate) { + alert("종료일이 시작일보다 이전일 수 없습니다."); + return; + } else { + setPagination({ + ...pagination, + pageIndex: 0, }); - }; + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, + isSucc, + startDate, + endDate, + }); + } + } else { + alert("조회 날짜를 선택해주세요."); + return; + } + }; - const handleSubmit = async (e?: React.FormEvent) => { - e?.preventDefault(); - - if (startDate && endDate) { - - if (startDate > endDate) { - alert('종료일이 시작일보다 이전일 수 없습니다.'); - return; - } else { - setPagination({ - ...pagination, - pageIndex: 0, - }); - setLinkSearchParam({ - ...linkSearchParam, - linkSn, - linkNm, - isSucc, - startDate, - endDate, - }); - } - } else { - alert('조회 날짜를 선택해주세요.'); - return; - } - }; - - return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - - - setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> - - setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> - - - -
-
-
-

로그 목록 (검색 조건에 따른 건수 : {query.data?.totalElements.toLocaleString('ko-KR') ?? 0} )

-
-
- {query.isLoading ? -
-
- -
-
- : -
{tableComponent}
- } -
-
+ return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + + + setStartDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + setEndDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + + +
+
+
+

+ 로그 목록 (검색 조건에 따른 건수 :{" "} + {query.data?.totalElements.toLocaleString("ko-KR") ?? 0} ) +

- ) -}; +
+ {query.isLoading ? ( +
+
+ +
+
+ ) : ( +
{tableComponent}
+ )} +
+
+
+ ); +} -export { LinkLogPage }; \ No newline at end of file +export { LinkLogPage }; diff --git a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx index 319df80b2..001eb9953 100644 --- a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx +++ b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx @@ -1,20 +1,31 @@ -'use client'; +"use client"; import { useState, useMemo } from "react"; import { - Button, - DataTable, - Input, - Label, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - LoadingWithAnimation, + Button, + DataTable, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + LoadingWithAnimation, } from "@common/components"; -import { getCoreRowModel, useReactTable, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; -import { LinkApi, type Link, type LinkSearchParam, type LinkVrblFormData, linkVrblFormSchema } from "@/entities/link"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + LinkApi, + type Link, + type LinkSearchParam, + type LinkVrblFormData, + linkVrblFormSchema, +} from "@/entities/link"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { columns } from "../model/table-columns"; import { vrblColumns } from "../model/vrbl-table-columns"; @@ -31,376 +42,388 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { useDataTable } from "@/shared/lib"; function LinkParamsPage() { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - const [linkSn, setLinkSn] = useState(''); - const [linkNm, setLinkNm] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '' + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + }); + + const [selectedLink, setSelectedLink] = useState<{ + linkSn: string; + linkNm: string; + }>({ + linkSn: "", + linkNm: "", + }); + + const [rowSelection, setRowSelection] = useState({}); + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); + + const { confirm } = useConfirm(); + + const [selectedVrblIds, setSelectedVrblIds] = useState([]); + const [isDBdata, setIsDBData] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [onConfirm, setOnConfirm] = useState(false); + + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + const { setMessage: alert } = useAlertStore((state) => state); + + const dbIdList = ["link0001", "link0002", "link0007"]; + + // Link 목록 쿼리 + const query = useQuery({ + queryKey: ["api/link/getLinkList", linkSearchParam], + queryFn: () => + LinkApi.getLinkList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + // LinkVrbl 목록 쿼리 - 별도 관리 + const linkVrblQuery = useQuery({ + queryKey: ["linkVrblList", selectedLink.linkSn], + queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn), + enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn), + staleTime: 0, // 항상 최신 데이터를 가져오도록 설정 + }); + + const handleResetSearchParam = () => { + setSelectedLink({ + linkSn: "", + linkNm: "", }); - const [selectedLink, setSelectedLink] = useState<{ - linkSn: string; - linkNm: string; - }>({ - linkSn: '', - linkNm: '' + setLinkSn(""); + setLinkNm(""); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + }); + }; + + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; + + const { mutateAsync: insertLinkVrbl } = useMutation({ + mutationFn: async (data: LinkVrblFormData) => + await LinkApi.insertLinkVrbl({ + ...data, + link_sn: selectedLink.linkSn, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + // 쿼리 무효화를 통한 최신 데이터 가져오기 + await queryClient.invalidateQueries({ + queryKey: ["linkVrblList", selectedLink.linkSn], + }); + setDialogOpen(false); + alert("저장되었습니다."); + }, + }); + + const { mutateAsync: updateLinkVrbl } = useMutation({ + mutationFn: async (data: LinkVrblFormData) => + await LinkApi.updateLinkVrblInfo({ + ...data, + link_sn: selectedLink.linkSn, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ["linkVrblList", selectedLink.linkSn], + }); + setDialogOpen(false); + alert("저장되었습니다."); + }, + }); + + const { mutateAsync: deleteLinkVrbl } = useMutation({ + mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data), + onSuccess: async () => { + await queryClient.refetchQueries({ + queryKey: ["linkVrblList", selectedLink.linkSn], + }); + + setSelectedVrblIds([]); + alert("삭제되었습니다."); + }, + }); + + // linkVrblQuery의 데이터를 사용 + const tableData = useMemo(() => { + return linkVrblQuery.data?.content ?? []; + }, [linkVrblQuery.data]); + + const vrblCols = useMemo( + () => + vrblColumns({ + selectedVrblIds, + setSelectedVrblIds, + }), + [selectedVrblIds], + ); + + const vrblTable = useReactTable({ + data: tableData, + columns: vrblCols, + getCoreRowModel: getCoreRowModel(), + }); + + const form = useForm({ + resolver: zodResolver(linkVrblFormSchema), + defaultValues: {}, + }); + + const handleRowClick = async (link: Link) => { + setSelectedLink({ + linkSn: link.link_sn, + linkNm: link.link_nm, }); - const [rowSelection, setRowSelection] = useState({}); + setSelectedVrblIds([]); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, + if (dbIdList.includes(link.link_clsf_cd)) { + setIsDBData(true); + } else { + setIsDBData(false); + } + }; + + const handleSubmit = (e: React.SubmitEvent) => { + e.preventDefault(); + + setSelectedLink({ + linkSn: "", + linkNm: "", }); - const { confirm } = useConfirm(); - - const [selectedVrblIds, setSelectedVrblIds] = useState([]); - const [isDBdata, setIsDBData] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [onConfirm, setOnConfirm] = useState(false); - - const userUniqId = getCookie(AUTH_USER_ID) ?? ""; - const { setMessage: alert } = useAlertStore((state) => state); - - const dbIdList = ['link0001', 'link0002', 'link0007']; - - // Link 목록 쿼리 - const query = useQuery({ - queryKey: ['api/link/getLinkList', linkSearchParam], - queryFn: () => LinkApi.getLinkList({ - ...linkSearchParam, - page: pagination.pageIndex, - size: pagination.pageSize, - }), + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, }); + }; - // LinkVrbl 목록 쿼리 - 별도 관리 - const linkVrblQuery = useQuery({ - queryKey: ['linkVrblList', selectedLink.linkSn], - queryFn: () => LinkApi.getLinkVrblList(selectedLink.linkSn), - enabled: !!selectedLink.linkSn && !dbIdList.includes(selectedLink.linkSn), - staleTime: 0, // 항상 최신 데이터를 가져오도록 설정 - }); + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; - const handleResetSearchParam = () => { - setSelectedLink({ - linkSn: '', - linkNm: '' - }); - - setLinkSn(''); - setLinkNm(''); - setLinkSearchParam({ - ...linkSearchParam, - linkSn: '', - linkNm: '' - }); - }; - - const handleConfirm = async (callback: () => Promise) => { - setOnConfirm(true); - await callback(); - setOnConfirm(false); - }; - - const { mutateAsync: insertLinkVrbl } = useMutation({ - mutationFn: async (data: LinkVrblFormData) => - await LinkApi.insertLinkVrbl({ - ...data, - link_sn: selectedLink.linkSn, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - // 쿼리 무효화를 통한 최신 데이터 가져오기 - await queryClient.invalidateQueries({ - queryKey: ['linkVrblList', selectedLink.linkSn] - }); - setDialogOpen(false); - alert('저장되었습니다.'); + const handleDeleteParam = async () => { + if (selectedVrblIds.length === 0) { + alert("삭제할 파라미터를 선택해 주세요."); + } else { + await handleConfirm(async () => { + if (await confirm("선택한 파라미터 정보를 삭제하시겠습니까?")) { + deleteLinkVrbl(selectedVrblIds); } - }); + }); + } + }; - const { mutateAsync: updateLinkVrbl } = useMutation({ - mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({ - ...data, - link_sn: selectedLink.linkSn, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: ['linkVrblList', selectedLink.linkSn] - }); - setDialogOpen(false); - alert('저장되었습니다.'); - } - }); + const handleVrblRowClick = async (data: LinkVrbl) => { + const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn); - const { mutateAsync: deleteLinkVrbl } = useMutation({ - mutationFn: async (data: string[]) => await LinkApi.deleteLinkVrbl(data), - onSuccess: async () => { - await queryClient.refetchQueries({ - queryKey: ['linkVrblList', selectedLink.linkSn] - }); - - setSelectedVrblIds([]); - alert('삭제되었습니다.'); - } - }); + if (linkVrblInfo) { + let newData = { ...linkVrblInfo }; - // linkVrblQuery의 데이터를 사용 - const tableData = useMemo(() => { - return linkVrblQuery.data?.content ?? []; - }, [linkVrblQuery.data]); + if (linkVrblInfo.mdfr_id) { + const userName = await fetchUserInfo(); + newData = { + ...linkVrblInfo, + mdfr_id: userName, + }; + } - const vrblCols = useMemo( - () => - vrblColumns({ - selectedVrblIds, - setSelectedVrblIds, - }), - [selectedVrblIds] - ); + form.reset(newData); + } + setDialogOpen(true); + }; - const vrblTable = useReactTable({ - data: tableData, - columns: vrblCols, - getCoreRowModel: getCoreRowModel(), - }); + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); - const form = useForm({ - resolver: zodResolver(linkVrblFormSchema), - defaultValues: {}, - }); + // LinkVrbl 데이터 표시 로직 + const renderLinkVrblContent = () => { + if (isDBdata) { + return ( +
+ 변수를 지원하지 않는 연계 데이터입니다. +
+ ); + } - const handleRowClick = async (link: Link) => { - setSelectedLink({ - linkSn: link.link_sn, - linkNm: link.link_nm, - }); + if (!selectedLink.linkSn) { + return ( +
+ 연계 데이터를 선택해주세요. +
+ ); + } - setSelectedVrblIds([]); - - if (dbIdList.includes(link.link_clsf_cd)) { - setIsDBData(true); - } else { - setIsDBData(false); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - setSelectedLink({ - linkSn: '', - linkNm: '' - }); - - setLinkSearchParam({ - ...linkSearchParam, - linkSn, - linkNm - }); - }; - - const fetchUserInfo = async () => { - const userData = await getUserInfo(); - return userData.userName; - }; - - const handleDeleteParam = async () => { - if (selectedVrblIds.length === 0) { - alert('삭제할 파라미터를 선택해 주세요.'); - } else { - await handleConfirm(async () => { - if (await confirm('선택한 파라미터 정보를 삭제하시겠습니까?')) { - deleteLinkVrbl(selectedVrblIds); - } - }); - } - }; - - const handleVrblRowClick = async (data: LinkVrbl) => { - const linkVrblInfo = await LinkApi.getLinkVrblInfo(data.link_vrbl_sn); - - if (linkVrblInfo) { - let newData = { ...linkVrblInfo }; - - if (linkVrblInfo.mdfr_id) { - const userName = await fetchUserInfo(); - newData = { - ...linkVrblInfo, - mdfr_id: userName, - }; - } - - form.reset(newData); - } - setDialogOpen(true); - }; - - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - rowSelection, - }, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - }, - dataTableOptions: { - onRowClick: handleRowClick, - viewPagination: true, - }, - }); - - // LinkVrbl 데이터 표시 로직 - const renderLinkVrblContent = () => { - if (isDBdata) { - return ( -
- 변수를 지원하지 않는 연계 데이터입니다. -
- ); - } - - if (!selectedLink.linkSn) { - return ( -
- 연계 데이터를 선택해주세요. -
- ); - } - - if (linkVrblQuery.isLoading) { - return ( -
-
- -
-
- ); - } - - return ( - <> -
- {tableData.length > 0 && ( - - )} - -
- {tableData.length > 0 ? ( -
- -
- ) : ( -
- 등록된 연계 변수가 없습니다. -
- )} - - ); - }; + if (linkVrblQuery.isLoading) { + return ( +
+
+ +
+
+ ); + } return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - -
-
-
-
-
-

연계 목록

-
-
- {!query.isLoading ? ( -
{tableComponent}
- ) : ( -
-
- -
-
- )} -
-
-
-
-

연계 변수 정보

-
-
- - - - - - - - 연계 일련번호 - - {selectedLink.linkSn ? selectedLink.linkSn : '-'} - - - - 연계 명 - - {selectedLink.linkNm ? selectedLink.linkNm : '-'} - - - -
- {renderLinkVrblContent()} -
-
-
- + <> +
+ {tableData.length > 0 && ( + + )} +
+ {tableData.length > 0 ? ( +
+ +
+ ) : ( +
+ 등록된 연계 변수가 없습니다. +
+ )} + ); + }; + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+
+

연계 변수 정보

+
+
+ + + + + + + + 연계 일련번호 + + {selectedLink.linkSn ? selectedLink.linkSn : "-"} + + + + 연계 명 + + {selectedLink.linkNm ? selectedLink.linkNm : "-"} + + + +
+ {renderLinkVrblContent()} +
+
+
+ +
+ ); } -export { LinkParamsPage }; \ No newline at end of file +export { LinkParamsPage }; diff --git a/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx index f6168ba76..19163d24c 100644 --- a/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx +++ b/apps/cs/src/pages/linkStats/ui/link-stats-page.tsx @@ -3,376 +3,429 @@ import { useState, useEffect, useRef } from "react"; import dayjs from "dayjs"; import { LinkApi, type LinkStats, type LinkSearchParam } from "@/entities/link"; -import { Button, Label, DatePicker, LoadingWithAnimation } from "@common/components"; import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Tooltip, - Legend, - type ChartOptions, - type Chart, + Button, + Label, + DatePicker, + LoadingWithAnimation, +} from "@common/components"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Tooltip, + Legend, + type ChartOptions, + type Chart, } from "chart.js"; import { Line } from "react-chartjs-2"; -import { getCoreRowModel, type PaginationState, type RowSelectionState, } from "@tanstack/react-table"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; import { columns } from "@/pages/linkLog/model/table-columns"; import { useDataTable } from "@/shared/lib"; import { useQuery } from "@tanstack/react-query"; interface LinkStatsListObject { - linkSuccStats: LinkStats[] | null; - linkFailStats: LinkStats[] | null; + linkSuccStats: LinkStats[] | null; + linkFailStats: LinkStats[] | null; } function LinkStatsPage() { + const succChartRef = useRef | null>(null); + const failChartRef = useRef | null>(null); - const succChartRef = useRef | null>(null); - const failChartRef = useRef | null>(null); + const [startDate, setStartDate] = useState( + dayjs().subtract(7, "day").format("YYYY-MM-DD"), + ); + const [endDate, setEndDate] = useState(dayjs().format("YYYY-MM-DD")); - const [startDate, setStartDate] = useState(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - const [endDate, setEndDate] = useState(dayjs().format('YYYY-MM-DD')); + const [rowSelection, setRowSelection] = useState({}); - const [rowSelection, setRowSelection] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, + const [linkStatsList, setLinkStatsList] = + useState(null); + const [logQueryParam, setLogQueryParam] = useState<{ + isSucc: "SUCC" | "FAIL"; + linkSn?: string; + linkNm?: string; + } | null>(null); + + const query = useQuery({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, + logQueryParam, + pagination, + ], + queryFn: () => + LinkApi.getLinkLogList({ + ...logQueryParam, + page: pagination.pageIndex, + size: pagination.pageSize, + startDate, + endDate, + }), + enabled: !!logQueryParam, + }); + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + startDate: "", + endDate: "", + }); + + ChartJS.register( + LineElement, + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend, + ); + + const succData = { + labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm), + datasets: [ + { + label: "연계 성공", + data: linkStatsList?.linkSuccStats?.map((e) => e.count), + fill: false, + borderColor: "rgb(53, 162, 235)", + backgroundColor: "rgb(53, 162, 235)", + tension: 0.1, + pointRadius: 6, + pointHoverRadius: 8, + }, + ], + }; + + const succOptions: ChartOptions<"line"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top", + }, + }, + scales: { + y: { + min: 0, + }, + }, + }; + + const failData = { + labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm), + datasets: [ + { + label: "연계 실패", + data: linkStatsList?.linkFailStats?.map((e) => e.count), + fill: false, + borderColor: "rgb(255, 99, 132)", + backgroundColor: "rgb(255, 99, 132)", + tension: 0.1, + pointRadius: 6, + pointHoverRadius: 8, + }, + ], + }; + + const failOptions: ChartOptions<"line"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top", + }, + }, + scales: { + y: { + min: 0, + }, + }, + }; + + const handleResetSearchParam = async () => { + setLinkSearchParam({ + linkSn: "", + linkNm: "", + startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"), + endDate: dayjs().format("YYYY-MM-DD"), }); - const [linkStatsList, setLinkStatsList] = useState(null); - const [logQueryParam, setLogQueryParam] = useState<{ - isSucc: 'SUCC' | 'FAIL'; - linkSn?: string; - linkNm?: string; - } | null>(null); - - const query = useQuery({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkList`, logQueryParam, pagination], - queryFn: () => - LinkApi.getLinkLogList({ - ...logQueryParam, - page: pagination.pageIndex, - size: pagination.pageSize, - startDate, - endDate, - }), - enabled: !!logQueryParam, + setLogQueryParam({ + isSucc: "SUCC", + linkNm: "", + linkSn: "", }); + setStartDate(dayjs().subtract(7, "day").format("YYYY-MM-DD")); + setEndDate(dayjs().format("YYYY-MM-DD")); - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - rowSelection, - }, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - }, - dataTableOptions: { - viewPagination: true, - }, + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + startDate: dayjs().subtract(7, "day").format("YYYY-MM-DD"), + endDate: dayjs().format("YYYY-MM-DD"), }); + setLinkStatsList(data); + }; - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '', - startDate: '', - endDate: '', - }); + const handleSubmit = async (e?: React.SubmitEvent) => { + e?.preventDefault(); - ChartJS.register( - LineElement, - CategoryScale, - LinearScale, - PointElement, - Tooltip, - Legend + if (startDate && endDate) { + if (startDate > endDate) { + alert("종료일이 시작일보다 이전일 수 없습니다."); + return; + } else { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate, + endDate, + }); + setLinkStatsList(data); + setLogQueryParam({ + isSucc: "SUCC", + linkNm: "", + linkSn: "", + }); + setLinkSearchParam({ + ...linkSearchParam, + startDate, + endDate, + }); + } + } else { + alert("조회 날짜를 선택해주세요."); + return; + } + }; + + const handleSuccChartClick = async ( + event: React.MouseEvent, + ) => { + if (!succChartRef.current) return; + + const chart = succChartRef.current; + + const points = chart.getElementsAtEventForMode( + event.nativeEvent, + "nearest", + { intersect: true }, + false, ); - const succData = { - labels: linkStatsList?.linkSuccStats?.map((e) => e.link_nm), - datasets: [ - { - label: '연계 성공', - data: linkStatsList?.linkSuccStats?.map((e) => e.count), - fill: false, - borderColor: 'rgb(53, 162, 235)', - backgroundColor: 'rgb(53, 162, 235)', - tension: 0.1, - pointRadius: 6, - pointHoverRadius: 8, - }, - ], + if (points[0] && linkStatsList?.linkSuccStats) { + const index = points[0].index; + setPagination({ + ...pagination, + pageIndex: 0, + pageSize: 100, + }); + setLogQueryParam({ + isSucc: "SUCC", + linkNm: linkStatsList.linkSuccStats[index]?.link_nm, + linkSn: linkStatsList.linkSuccStats[index]?.link_sn, + }); + } + }; + + const handleFailChartClick = async ( + event: React.MouseEvent, + ) => { + if (!failChartRef.current) return; + + const chart = failChartRef.current; + + const points = chart.getElementsAtEventForMode( + event.nativeEvent, + "nearest", + { intersect: true }, + false, + ); + + if (points[0] && linkStatsList?.linkFailStats) { + const index = points[0].index; + setPagination({ + ...pagination, + pageIndex: 0, + pageSize: 100, + }); + setLogQueryParam({ + isSucc: "FAIL", + linkNm: linkStatsList.linkFailStats[index]?.link_nm, + linkSn: linkStatsList.linkFailStats[index]?.link_sn, + }); + } + }; + + const handleSubmitToday = async () => { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate: dayjs().format("YYYY-MM-DD"), + endDate: dayjs().format("YYYY-MM-DD"), + }); + setEndDate(dayjs().format("YYYY-MM-DD")); + setStartDate(dayjs().format("YYYY-MM-DD")); + setLinkStatsList(data); + setLinkSearchParam({ + ...linkSearchParam, + startDate: dayjs().format("YYYY-MM-DD"), + endDate: dayjs().format("YYYY-MM-DD"), + }); + setLogQueryParam({ + isSucc: "SUCC", + linkNm: "", + linkSn: "", + }); + }; + + useEffect(() => { + const fetchData = async () => { + const data = await LinkApi.getLinkStatsList({ + ...linkSearchParam, + startDate, + endDate, + }); + setLinkStatsList(data); }; - const succOptions: ChartOptions<'line'> = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top', - }, - }, - scales: { - y: { - min: 0, - }, - }, - }; + setLinkSearchParam({ + ...linkSearchParam, + startDate, + endDate, + }); - const failData = { - labels: linkStatsList?.linkFailStats?.map((e) => e.link_nm), - datasets: [ - { - label: '연계 실패', - data: linkStatsList?.linkFailStats?.map((e) => e.count), - fill: false, - borderColor: 'rgb(255, 99, 132)', - backgroundColor: 'rgb(255, 99, 132)', - tension: 0.1, - pointRadius: 6, - pointHoverRadius: 8, - }, - ], - }; + fetchData(); + }, []); - const failOptions: ChartOptions<'line'> = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top', - }, - }, - scales: { - y: { - min: 0, - }, - }, - }; - - const handleResetSearchParam = async () => { - setLinkSearchParam({ - linkSn: '', - linkNm: '', - startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), - endDate: dayjs().format('YYYY-MM-DD'), - }); - - setLogQueryParam({ - isSucc: 'SUCC', - linkNm: '', - linkSn: '', - }); - - setStartDate(dayjs().subtract(7, "day").format('YYYY-MM-DD')); - setEndDate(dayjs().format('YYYY-MM-DD')); - - const data = await LinkApi.getLinkStatsList({ - ...linkSearchParam, - linkSn: '', - linkNm: '', - startDate: dayjs().subtract(7, "day").format('YYYY-MM-DD'), - endDate: dayjs().format('YYYY-MM-DD'), - }); - setLinkStatsList(data); - }; - - const handleSubmit = async (e?: React.FormEvent) => { - e?.preventDefault(); - - if (startDate && endDate) { - - if (startDate > endDate) { - alert('종료일이 시작일보다 이전일 수 없습니다.'); - return; - } else { - const data = await LinkApi.getLinkStatsList({ - ...linkSearchParam, - startDate, - endDate - }); - setLinkStatsList(data); - setLogQueryParam({ - isSucc: 'SUCC', - linkNm: '', - linkSn: '', - }); - setLinkSearchParam({ - ...linkSearchParam, - startDate, - endDate - }); - } - } else { - alert('조회 날짜를 선택해주세요.'); - return; - } - }; - - const handleSuccChartClick = async (event: React.MouseEvent) => { - if (!succChartRef.current) return; - - const chart = succChartRef.current; - - const points = chart.getElementsAtEventForMode( - event.nativeEvent, - "nearest", - { intersect: true }, - false - ); - - if (points[0] && linkStatsList?.linkSuccStats) { - const index = points[0].index; - setPagination({ - ...pagination, - pageIndex: 0, - pageSize: 100, - }); - setLogQueryParam({ - isSucc: 'SUCC', - linkNm: linkStatsList.linkSuccStats[index]?.link_nm, - linkSn: linkStatsList.linkSuccStats[index]?.link_sn, - }); - } - }; - - const handleFailChartClick = async (event: React.MouseEvent) => { - if (!failChartRef.current) return; - - const chart = failChartRef.current; - - const points = chart.getElementsAtEventForMode( - event.nativeEvent, - "nearest", - { intersect: true }, - false - ); - - if (points[0] && linkStatsList?.linkFailStats) { - const index = points[0].index; - setPagination({ - ...pagination, - pageIndex: 0, - pageSize: 100, - }); - setLogQueryParam({ - isSucc: 'FAIL', - linkNm: linkStatsList.linkFailStats[index]?.link_nm, - linkSn: linkStatsList.linkFailStats[index]?.link_sn, - }); - } - }; - - const handleSubmitToday = async () => { - const data = await LinkApi.getLinkStatsList({ - ...linkSearchParam, - startDate: dayjs().format('YYYY-MM-DD'), - endDate: dayjs().format('YYYY-MM-DD'), - }); - setEndDate(dayjs().format('YYYY-MM-DD')); - setStartDate(dayjs().format('YYYY-MM-DD')); - setLinkStatsList(data); - setLinkSearchParam({ - ...linkSearchParam, - startDate: dayjs().format('YYYY-MM-DD'), - endDate: dayjs().format('YYYY-MM-DD'), - }); - setLogQueryParam({ - isSucc: 'SUCC', - linkNm: '', - linkSn: '', - }); - }; - - useEffect(() => { - const fetchData = async () => { - const data = await LinkApi.getLinkStatsList({ - ...linkSearchParam, - startDate, - endDate - }); - setLinkStatsList(data); - }; - - setLinkSearchParam({ - ...linkSearchParam, - startDate, - endDate - }); - - fetchData(); - }, []); - - return ( -
-
-
- - setStartDate(dayjs(e).format('YYYY-MM-DD'))} /> - - setEndDate(dayjs(e).format('YYYY-MM-DD'))} /> - - - - -
-
-
-
-

조회 일자 : {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`}

-
-
- -
-
- -
-
-
-
-

- {logQueryParam?.linkNm ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === 'FAIL' ? '실패' : '성공'} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString('ko-KR') ?? 0}건` : '로그 목록'} -

-
-
- {logQueryParam?.linkNm ? - query.isLoading ? -
-
- -
-
- : -
{tableComponent}
- : -
- 차트 데이터를 선택해주세요. -
- } -
-
-
+ return ( +
+
+
+ + setStartDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + setEndDate(dayjs(e).format("YYYY-MM-DD"))} + /> + + + + +
+
+
+
+

+ 조회 일자 :{" "} + {`${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}`} +

+
+
+ +
+
+ +
- ) -}; +
+
+

+ {logQueryParam?.linkNm + ? `${logQueryParam.linkNm} 연계 ${logQueryParam.isSucc === "FAIL" ? "실패" : "성공"} 로그 목록 (${linkSearchParam.startDate} ~ ${linkSearchParam.endDate}) - ${query.data?.totalElements.toLocaleString("ko-KR") ?? 0}건` + : "로그 목록"} +

+
+
+ {logQueryParam?.linkNm ? ( + query.isLoading ? ( +
+
+ +
+
+ ) : ( +
{tableComponent}
+ ) + ) : ( +
+ 차트 데이터를 선택해주세요. +
+ )} +
+
+
+
+ ); +} -export { LinkStatsPage }; \ No newline at end of file +export { LinkStatsPage }; diff --git a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx index 0394e7bf6..3ccee78e8 100644 --- a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx +++ b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx @@ -1,20 +1,35 @@ -'use client'; +"use client"; import { useState, useMemo } from "react"; import { - Button, - DataTable, - Input, - Label, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - LoadingWithAnimation, + Button, + DataTable, + Input, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + LoadingWithAnimation, } from "@common/components"; -import { getCoreRowModel, type PaginationState, type RowSelectionState, useReactTable } from "@tanstack/react-table"; -import { LinkApi, type Link, type LinkSearchParam, type LinkTable, type LinkTableColumns, type LinkTableColData, type LinkTableFormData, LinkTableColumnsSchema, LinkTableSchema } from "@/entities/link"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; +import { + LinkApi, + type Link, + type LinkSearchParam, + type LinkTable, + type LinkTableColumns, + type LinkTableColData, + type LinkTableFormData, + LinkTableColumnsSchema, + LinkTableSchema, +} from "@/entities/link"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { columns } from "../../linkParams/model/table-columns"; import { tableColumns } from "../model/table-columns"; @@ -31,680 +46,778 @@ import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form"; import { useDataTable } from "@/shared/lib"; function LinkTablePage() { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + const userUniqId = getCookie(AUTH_USER_ID) ?? ""; - const [linkSn, setLinkSn] = useState(''); - const [linkNm, setLinkNm] = useState(''); + const [linkSn, setLinkSn] = useState(""); + const [linkNm, setLinkNm] = useState(""); - const [linkSearchParam, setLinkSearchParam] = useState({ - linkSn: '', - linkNm: '' - }); + const [linkSearchParam, setLinkSearchParam] = useState({ + linkSn: "", + linkNm: "", + }); - const [selectedTableInfo, setSelectedTableInfo] = useState(null); - const [tableColList, setTableColList] = useState([]); - const [dialogOpen, setDialogOpen] = useState(false); - const [selectedColIds, setSelectedColIds] = useState([]); + const [selectedTableInfo, setSelectedTableInfo] = useState( + null, + ); + const [tableColList, setTableColList] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedColIds, setSelectedColIds] = useState([]); - const [rowSelection, setRowSelection] = useState({}); + const [rowSelection, setRowSelection] = useState({}); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 100, - }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 100, + }); - const { setMessage: alert } = useAlertStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); - const { confirm } = useConfirm(); - const [onConfirm, setOnConfirm] = useState(false); + const { confirm } = useConfirm(); + const [onConfirm, setOnConfirm] = useState(false); - const handleConfirm = async (callback: () => Promise) => { - setOnConfirm(true); - await callback(); - setOnConfirm(false); - }; + const handleConfirm = async (callback: () => Promise) => { + setOnConfirm(true); + await callback(); + setOnConfirm(false); + }; - const query = useQuery({ - queryKey: ['/api/link/getLinkList', linkSearchParam], - queryFn: () => LinkApi.getLinkList({ - ...linkSearchParam, - page: pagination.pageIndex, - size: pagination.pageSize, - }), - }); + const query = useQuery({ + queryKey: ["/api/link/getLinkList", linkSearchParam], + queryFn: () => + LinkApi.getLinkList({ + ...linkSearchParam, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); - const { mutateAsync: insertLinkTableCol } = useMutation({ - mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({ - ...data, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('저장되었습니다.'); - } - } - }); - - const { mutateAsync: updateLinkTableCol } = useMutation({ - mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({ - ...data, - mdfr_id: userUniqId, - }), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('수정되었습니다.'); - } - } - }); - - const { mutateAsync: deleteLinkTableCol } = useMutation({ - mutationFn: async (data: string[]) => await LinkApi.deleteLinkTableCol(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - setDialogOpen(false); - alert('삭제되었습니다.'); - } - } - }); - - const { mutateAsync: insertLinkTableInfo } = useMutation({ - mutationFn: async (data: LinkTableFormData) => await LinkApi.insertLinkTableInfo(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - tableForm.reset({ - link_sn: tableData.link_sn, - link_nm: tableData.link_nm, - tbl_sn: tableData.tbl_sn ?? "", - sou_tbl_nm: tableData.sou_tbl_nm ?? "", - sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", - sou_tbl_expln: tableData.sou_tbl_expln ?? "", - trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", - trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", - trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", - }); - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - alert('저장되었습니다.'); - } - } - }); - - const { mutateAsync: updateLinkTableInfo } = useMutation({ - mutationFn: async (data: LinkTableFormData) => await LinkApi.updateLinkTableInfo(data), - onSuccess: async () => { - if (selectedTableInfo) { - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`] - }); - await queryClient.invalidateQueries({ - queryKey: [`${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`] - }); - - const tableData = await LinkApi.getLinkTableInfo(selectedTableInfo.link_sn); - const tableColList = await LinkApi.getLinkTableColList(selectedTableInfo.link_sn); - - setSelectedTableInfo(tableData); - setTableColList(tableColList.content); - setSelectedColIds([]); - alert('수정되었습니다.'); - } - } - }); - - const tableCol = useMemo( - () => - tableColumns({ selectedColIds, setSelectedColIds }), - [selectedColIds] - ); - - const colTable = useReactTable({ - data: tableColList, - columns: tableCol, - getCoreRowModel: getCoreRowModel(), - }); - - const form = useForm({ - resolver: zodResolver(LinkTableColumnsSchema), - defaultValues: { - pk_yn: "N" - } - }); - - const tableForm = useForm({ - resolver: zodResolver(LinkTableSchema), - defaultValues: {}, - }); - - const handleResetSearchParam = async () => { - setSelectedTableInfo(null); - setTableColList([]); - setLinkSn(''); - setLinkNm(''); - setLinkSearchParam({ - ...linkSearchParam, - linkSn: '', - linkNm: '', + const { mutateAsync: insertLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => + await LinkApi.insertLinkTableCol({ + ...data, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], }); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setSelectedTableInfo(null); - setTableColList([]); - - setLinkSearchParam({ - ...linkSearchParam, - linkSn, - linkNm + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], }); - }; - const handleRowClick = async (data: Link) => { - const tableData = await LinkApi.getLinkTableInfo(data.link_sn); - const tableColList = await LinkApi.getLinkTableColList(data.link_sn); + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); setSelectedTableInfo(tableData); setTableColList(tableColList.content); setSelectedColIds([]); + setDialogOpen(false); + alert("저장되었습니다."); + } + }, + }); + + const { mutateAsync: updateLinkTableCol } = useMutation({ + mutationFn: async (data: LinkTableColData) => + await LinkApi.updateLinkTableCol({ + ...data, + mdfr_id: userUniqId, + }), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert("수정되었습니다."); + } + }, + }); + + const { mutateAsync: deleteLinkTableCol } = useMutation({ + mutationFn: async (data: string[]) => + await LinkApi.deleteLinkTableCol(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + setDialogOpen(false); + alert("삭제되었습니다."); + } + }, + }); + + const { mutateAsync: insertLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => + await LinkApi.insertLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); + + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); tableForm.reset({ - link_sn: tableData.link_sn, - link_nm: tableData.link_nm, - tbl_sn: tableData.tbl_sn ?? "", - sou_tbl_nm: tableData.sou_tbl_nm ?? "", - sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", - sou_tbl_expln: tableData.sou_tbl_expln ?? "", - trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", - trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", - trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", }); - }; + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert("저장되었습니다."); + } + }, + }); - const handleColumnRowClick = async (data: LinkTableColumns) => { - const colInfo = await LinkApi.getLinkTableColInfo({ colSn: data.col_sn, tblSn: data.tbl_sn }); + const { mutateAsync: updateLinkTableInfo } = useMutation({ + mutationFn: async (data: LinkTableFormData) => + await LinkApi.updateLinkTableInfo(data), + onSuccess: async () => { + if (selectedTableInfo) { + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableInfo`, + ], + }); + await queryClient.invalidateQueries({ + queryKey: [ + `${process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST}/getLinkTableColList`, + ], + }); - if (colInfo) { - let newData = { ...colInfo }; + const tableData = await LinkApi.getLinkTableInfo( + selectedTableInfo.link_sn, + ); + const tableColList = await LinkApi.getLinkTableColList( + selectedTableInfo.link_sn, + ); - if (colInfo.mdfr_id) { - const userName = await fetchUserInfo(); - newData = { - ...colInfo, - mdfr_id: userName, - }; - } + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + alert("수정되었습니다."); + } + }, + }); - form.reset(newData); + const tableCol = useMemo( + () => tableColumns({ selectedColIds, setSelectedColIds }), + [selectedColIds], + ); - } - setDialogOpen(true) - }; + const colTable = useReactTable({ + data: tableColList, + columns: tableCol, + getCoreRowModel: getCoreRowModel(), + }); - const fetchUserInfo = async () => { - const userData = await getUserInfo(); - return userData.userName; - }; + const form = useForm({ + resolver: zodResolver(LinkTableColumnsSchema), + defaultValues: { + pk_yn: "N", + }, + }); - const handleDeleteParam = async () => { - if (selectedColIds.length === 0) { - alert('삭제할 컬럼을 선택해 주세요.'); - } else { - await handleConfirm(async () => { - if (await confirm('선택한 컬럼 정보를 삭제하시겠습니까?')) { - deleteLinkTableCol(selectedColIds); - } - }); - } - }; + const tableForm = useForm({ + resolver: zodResolver(LinkTableSchema), + defaultValues: {}, + }); - const handleTableSubmit = async (data: LinkTableFormData) => { - const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm); - const confirmMsg = (regTblInfo.count > 0 && data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm) ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?` : `${!data.tbl_sn ? '저장' : '수정'}하시겠습니까?`; - if (!data.tbl_sn) { - await handleConfirm(async () => { - if (await confirm(confirmMsg)) { - insertLinkTableInfo({ - ...data, - mdfr_id: userUniqId, - tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", - }); - } - }); - } else { - await handleConfirm(async () => { - if (await confirm(confirmMsg)) { - updateLinkTableInfo({ - ...data, - tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", - mdfr_id: userUniqId, - }); - } - }); - } + const handleResetSearchParam = async () => { + setSelectedTableInfo(null); + setTableColList([]); + setLinkSn(""); + setLinkNm(""); + setLinkSearchParam({ + ...linkSearchParam, + linkSn: "", + linkNm: "", + }); + }; - }; + const handleSubmit = (e: React.SubmitEvent) => { + e.preventDefault(); + setSelectedTableInfo(null); + setTableColList([]); - const { tableComponent } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - rowSelection, - }, - manualPagination: true, - enableMultiRowSelection: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - }, - dataTableOptions: { - onRowClick: handleRowClick, - viewPagination: true, - }, + setLinkSearchParam({ + ...linkSearchParam, + linkSn, + linkNm, + }); + }; + + const handleRowClick = async (data: Link) => { + const tableData = await LinkApi.getLinkTableInfo(data.link_sn); + const tableColList = await LinkApi.getLinkTableColList(data.link_sn); + + setSelectedTableInfo(tableData); + setTableColList(tableColList.content); + setSelectedColIds([]); + + tableForm.reset({ + link_sn: tableData.link_sn, + link_nm: tableData.link_nm, + tbl_sn: tableData.tbl_sn ?? "", + sou_tbl_nm: tableData.sou_tbl_nm ?? "", + sou_tbl_korn_nm: tableData.sou_tbl_korn_nm ?? "", + sou_tbl_expln: tableData.sou_tbl_expln ?? "", + trsf_tbl_expln: tableData.trsf_tbl_expln ?? "", + trsf_tbl_nm: tableData.trsf_tbl_nm ?? "", + trsf_tbl_korn_nm: tableData.trsf_tbl_korn_nm ?? "", + }); + }; + + const handleColumnRowClick = async (data: LinkTableColumns) => { + const colInfo = await LinkApi.getLinkTableColInfo({ + colSn: data.col_sn, + tblSn: data.tbl_sn, }); - return ( -
-
-
- - setLinkSn(e.target.value)} - autoComplete="off" - /> - - setLinkNm(e.target.value)} - autoComplete="off" - /> - - -
-
-
-
-
-

연계 목록

-
-
- {!query.isLoading ? -
{tableComponent}
- : -
-
- -
-
- } -
-
-
-
-

연계 테이블 정보

-
-
-
- - {selectedTableInfo && -
- -
- } - - - - - - - - 연계 일련번호 - - {selectedTableInfo ? - ( - - - - - - )} - /> - : - - - } - - - - 연계 명 - - {selectedTableInfo ? - ( - - - - - - )} - /> - : - - - } - - - - 원천 테이블 명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_nm && ( -

- {tableForm.formState.errors.sou_tbl_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 원천 테이블 한글명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_korn_nm && ( -

- {tableForm.formState.errors.sou_tbl_korn_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 원천 테이블 설명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.sou_tbl_expln && ( -

- {tableForm.formState.errors.sou_tbl_expln.message} -

- )} -
- )} - /> - : - - - } -
-
- - * 변환 테이블 명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_nm && ( -

- {tableForm.formState.errors.trsf_tbl_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - * 변환 테이블 한글명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_korn_nm && ( -

- {tableForm.formState.errors.trsf_tbl_korn_nm.message} -

- )} -
- )} - /> - : - - - } -
-
- - 변환 테이블 설명 - - {selectedTableInfo ? - ( - - - - - {tableForm.formState.errors.trsf_tbl_expln && ( -

- {tableForm.formState.errors.trsf_tbl_expln.message} -

- )} -
- )} - /> - : - - - } -
-
-
-
-
- - {selectedTableInfo ? - <> -
- {tableColList.length > 0 && - - } - {selectedTableInfo.tbl_sn && - - } -
- {selectedTableInfo.tbl_sn ? - (tableColList.length > 0 ? -
- -
- : -
- 등록된 테이블 컬럼이 없습니다. -
) - : -
- 테이블 정보를 등록해주세요. -
- } - - : -
- 연계 데이터를 선택해주세요. -
- } -
-
-
- -
- ) -}; + if (colInfo) { + let newData = { ...colInfo }; -export { LinkTablePage }; \ No newline at end of file + if (colInfo.mdfr_id) { + const userName = await fetchUserInfo(); + newData = { + ...colInfo, + mdfr_id: userName, + }; + } + + form.reset(newData); + } + setDialogOpen(true); + }; + + const fetchUserInfo = async () => { + const userData = await getUserInfo(); + return userData.userName; + }; + + const handleDeleteParam = async () => { + if (selectedColIds.length === 0) { + alert("삭제할 컬럼을 선택해 주세요."); + } else { + await handleConfirm(async () => { + if (await confirm("선택한 컬럼 정보를 삭제하시겠습니까?")) { + deleteLinkTableCol(selectedColIds); + } + }); + } + }; + + const handleTableSubmit = async (data: LinkTableFormData) => { + const regTblInfo = await LinkApi.countLinkTblNm(data.trsf_tbl_nm); + const confirmMsg = + regTblInfo.count > 0 && + data.trsf_tbl_nm !== selectedTableInfo?.trsf_tbl_nm + ? `이미 사용중인 테이블 명입니다. 해당 테이블 명으로 ${!data.tbl_sn ? "저장" : "수정"}하시겠습니까?` + : `${!data.tbl_sn ? "저장" : "수정"}하시겠습니까?`; + if (!data.tbl_sn) { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + insertLinkTableInfo({ + ...data, + mdfr_id: userUniqId, + tbl_sn: regTblInfo.count > 0 ? regTblInfo.tbl_sn : "", + }); + } + }); + } else { + await handleConfirm(async () => { + if (await confirm(confirmMsg)) { + updateLinkTableInfo({ + ...data, + tbl_sn: regTblInfo.count > 0 ? regTblInfo.tbl_sn : "", + mdfr_id: userUniqId, + }); + } + }); + } + }; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }, + dataTableOptions: { + onRowClick: handleRowClick, + viewPagination: true, + }, + }); + + return ( +
+
+
+ + setLinkSn(e.target.value)} + autoComplete="off" + /> + + setLinkNm(e.target.value)} + autoComplete="off" + /> + + +
+
+
+
+
+

연계 목록

+
+
+ {!query.isLoading ? ( +
{tableComponent}
+ ) : ( +
+
+ +
+
+ )} +
+
+
+
+

+ 연계 테이블 정보 +

+
+
+
+ + {selectedTableInfo && ( +
+ +
+ )} + + + + + + + + 연계 일련번호 + + {selectedTableInfo ? ( + ( + + + + + + )} + /> + ) : ( + - + )} + + + + 연계 명 + + {selectedTableInfo ? ( + ( + + + + + + )} + /> + ) : ( + - + )} + + + + 원천 테이블 명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_nm && ( +

+ { + tableForm.formState.errors.sou_tbl_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 원천 테이블 한글명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_korn_nm && ( +

+ { + tableForm.formState.errors.sou_tbl_korn_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 원천 테이블 설명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.sou_tbl_expln && ( +

+ { + tableForm.formState.errors.sou_tbl_expln + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + * 변환 테이블 명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.trsf_tbl_nm && ( +

+ { + tableForm.formState.errors.trsf_tbl_nm + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + * 변환 테이블 한글명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors + .trsf_tbl_korn_nm && ( +

+ { + tableForm.formState.errors + .trsf_tbl_korn_nm.message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+ + 변환 테이블 설명 + + {selectedTableInfo ? ( + ( + + + + + {tableForm.formState.errors.trsf_tbl_expln && ( +

+ { + tableForm.formState.errors.trsf_tbl_expln + .message + } +

+ )} +
+ )} + /> + ) : ( + - + )} +
+
+
+
+
+ + {selectedTableInfo ? ( + <> +
+ {tableColList.length > 0 && ( + + )} + {selectedTableInfo.tbl_sn && ( + + )} +
+ {selectedTableInfo.tbl_sn ? ( + tableColList.length > 0 ? ( +
+ +
+ ) : ( +
+ 등록된 테이블 컬럼이 없습니다. +
+ ) + ) : ( +
+ 테이블 정보를 등록해주세요. +
+ )} + + ) : ( +
+ 연계 데이터를 선택해주세요. +
+ )} +
+
+
+ +
+ ); +} + +export { LinkTablePage }; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index d94d79c9a..baca00d74 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -77,7 +77,7 @@ function LoginlogPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setIsManualLoading(true); setPagination({ diff --git a/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx index a24b77308..78c34d440 100644 --- a/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/menu-history-dialog.tsx @@ -91,7 +91,7 @@ function MenuHistoryDialog({ menuCd }: MenuHistoryDialogProps) { }, }); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); query.refetch(); diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index eb942fe11..6956d6465 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -59,7 +59,7 @@ function OrganizationSearchDialog({ handleUserSelect(node.data as TreeUser); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); e.stopPropagation(); setTree(filterUserOrganizationTreeDataByKeyword(treeData ?? [], keyword)); diff --git a/apps/cs/src/pages/module/ui/module-page.tsx b/apps/cs/src/pages/module/ui/module-page.tsx index 8593ba4b5..1749bdfbd 100644 --- a/apps/cs/src/pages/module/ui/module-page.tsx +++ b/apps/cs/src/pages/module/ui/module-page.tsx @@ -62,7 +62,7 @@ function ModulePage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx index 1f2e0e9c4..80434a15f 100644 --- a/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx +++ b/apps/cs/src/pages/ntftalk-history/ui/ntftalk-history-page.tsx @@ -7,7 +7,7 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import dayjs from "dayjs"; import { ENDPOINT } from "@/shared/config"; -import { useDataTable } from "@/shared/lib"; +import { useAlertStore, useDataTable } from "@/shared/lib"; import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; import { NtftalkMmtLogHistoryApi, @@ -54,7 +54,14 @@ function computeDateRangeFromPreset( return { from: fromDay.startOf("day").toDate(), to }; } +function isDateRangeOverOneYear(range: DateRange): boolean { + if (!range.from) return false; + const oneYearAgo = dayjs().subtract(1, "year").startOf("day"); + return dayjs(range.from).isBefore(oneYearAgo); +} + function NtftalkHistoryPage() { + const alert = useAlertStore((state) => state.setMessage); const [contentInput, setContentInput] = useState(""); const [recipientInput, setRecipientInput] = useState(""); const [presetPeriod, setPresetPeriod] = useState(""); @@ -115,8 +122,12 @@ function NtftalkHistoryPage() { dateTo: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : undefined, }); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); + if (isDateRangeOverOneYear(dateRange)) { + alert("조회 기간은 오늘 기준 최대 1년까지만 선택할 수 있습니다."); + return; + } setPagination((p) => ({ ...p, pageIndex: 0 })); setSearchParams(buildSearchParamsFromInputs()); }; diff --git a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx index 57db5d6e2..787f33f93 100644 --- a/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx +++ b/apps/cs/src/pages/ntftalk-temp/ui/ntftalk-temp-page.tsx @@ -254,7 +254,7 @@ function NtftalkTempPage() { messageTypeFilter === ALL ? undefined : messageTypeFilter, }); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); setPagination((p) => ({ ...p, pageIndex: 0 })); setSearchParams(buildSearchParamsFromInputs()); diff --git a/apps/cs/src/pages/popup/ui/popup-page.tsx b/apps/cs/src/pages/popup/ui/popup-page.tsx index e55e5652b..3ce6d131a 100644 --- a/apps/cs/src/pages/popup/ui/popup-page.tsx +++ b/apps/cs/src/pages/popup/ui/popup-page.tsx @@ -48,7 +48,7 @@ function PopupPage() { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx b/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx index ffce1c92d..f5f26bb98 100644 --- a/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx +++ b/apps/cs/src/pages/privacylog/ui/privacylog-page.tsx @@ -77,7 +77,7 @@ function PrivacyLogPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx index a2b0eeb00..803317669 100644 --- a/apps/cs/src/pages/recent/ui/recent-page.tsx +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -127,7 +127,7 @@ function RecentPage() { const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setIsDeletePostData(delYn === "Y"); setPagination({ diff --git a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx index e327fb88e..165f5c942 100644 --- a/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx +++ b/apps/cs/src/pages/role/ui/mapping-role-dialog.tsx @@ -83,7 +83,7 @@ function MappingRoleDialog({ ); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const keyword = formData.get("keyword") as string; diff --git a/apps/cs/src/pages/role/ui/role-page.tsx b/apps/cs/src/pages/role/ui/role-page.tsx index 97c3d615b..47d3d9a9e 100644 --- a/apps/cs/src/pages/role/ui/role-page.tsx +++ b/apps/cs/src/pages/role/ui/role-page.tsx @@ -64,7 +64,7 @@ function RolePage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 046c80142..fbdd9d36c 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -35,7 +35,7 @@ function SitePage() { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setSearchParams({ ...searchParams, diff --git a/apps/cs/src/pages/survey/ui/survey-page.tsx b/apps/cs/src/pages/survey/ui/survey-page.tsx index bce024f4f..27f080343 100644 --- a/apps/cs/src/pages/survey/ui/survey-page.tsx +++ b/apps/cs/src/pages/survey/ui/survey-page.tsx @@ -71,7 +71,7 @@ function SurveyPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/template/ui/template-page.tsx b/apps/cs/src/pages/template/ui/template-page.tsx index de3dca8ff..d4ceb3bca 100644 --- a/apps/cs/src/pages/template/ui/template-page.tsx +++ b/apps/cs/src/pages/template/ui/template-page.tsx @@ -37,7 +37,7 @@ function TemplatePage() { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setSearchParams({ ...searchParams, diff --git a/apps/cs/src/pages/terms/ui/terms-page.tsx b/apps/cs/src/pages/terms/ui/terms-page.tsx index 08a6e2080..ea00276df 100644 --- a/apps/cs/src/pages/terms/ui/terms-page.tsx +++ b/apps/cs/src/pages/terms/ui/terms-page.tsx @@ -38,7 +38,7 @@ function TermsPage() { placeholderData: keepPreviousData, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx b/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx index b4c821551..d562d0103 100644 --- a/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx +++ b/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx @@ -148,7 +148,7 @@ function UserStatisticsPage() { setPresetDateRange(""); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); const params: ConnectionStatisticsParams = { keywordType, diff --git a/apps/cs/src/pages/users/ui/users-page.tsx b/apps/cs/src/pages/users/ui/users-page.tsx index 5362b8b42..5670eb9d3 100644 --- a/apps/cs/src/pages/users/ui/users-page.tsx +++ b/apps/cs/src/pages/users/ui/users-page.tsx @@ -102,7 +102,7 @@ function UsersPage() { }, }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setPagination({ ...pagination, diff --git a/apps/cs/src/widgets/post/ui/post-page.tsx b/apps/cs/src/widgets/post/ui/post-page.tsx index 850513a94..4b6e374c0 100644 --- a/apps/cs/src/widgets/post/ui/post-page.tsx +++ b/apps/cs/src/widgets/post/ui/post-page.tsx @@ -135,7 +135,7 @@ function PostPage({ bbsId }: { bbsId: string }) { refetch(); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); refetch(); }; diff --git a/apps/dc/src/pages/auth/login/ui/login-page.tsx b/apps/dc/src/pages/auth/login/ui/login-page.tsx index 92f5935ed..0164e4327 100644 --- a/apps/dc/src/pages/auth/login/ui/login-page.tsx +++ b/apps/dc/src/pages/auth/login/ui/login-page.tsx @@ -6,7 +6,7 @@ import { LoginApi } from "@/entities/auth"; import "./login.css"; function LoginPage() { - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); diff --git a/apps/dc/src/pages/category/ui/category-page.tsx b/apps/dc/src/pages/category/ui/category-page.tsx index 29f8ef71b..dfb262698 100644 --- a/apps/dc/src/pages/category/ui/category-page.tsx +++ b/apps/dc/src/pages/category/ui/category-page.tsx @@ -39,8 +39,10 @@ type TreeNode = { function CategoryPage() { const router = useRouter(); const [treeData, setTreeData] = useState[]>([]); - const [selectedParentNode, setSelectedParentNode] = useState | null>(null); - const [selectedSubNode, setSelectedSubNode] = useState | null>(null); + const [selectedParentNode, setSelectedParentNode] = + useState | null>(null); + const [selectedSubNode, setSelectedSubNode] = + useState | null>(null); const [categoryQuery, setSearchText] = useState({ clsfSystmNm: "", }); @@ -48,7 +50,9 @@ function CategoryPage() { const [loading, setLoading] = useState(false); const { setMessage: showAlert } = useAlertStore((state) => state); const { confirm } = useConfirm(); - const [dialog, setDialog] = useState(null); + const [dialog, setDialog] = useState(null); const userUnqId = useUserStore((state) => state.userInfo?.userUnqId || ""); const userInfo = useUserStore((state) => state.userInfo); const [expandedIds, setExpandedIds] = useState<(string | number)[]>([]); @@ -57,7 +61,8 @@ function CategoryPage() { const categoryNameInputRef = useRef(null); const [showCategoryNameError, setShowCategoryNameError] = useState(false); const [isAddingMode, setIsAddingMode] = useState(false); - const [addingParentNode, setAddingParentNode] = useState | null>(null); + const [addingParentNode, setAddingParentNode] = + useState | null>(null); const { data: categoryList, refetch } = useQuery({ queryKey: [ENDPOINT.DC_SERVICE.CATEGORY], @@ -70,20 +75,18 @@ function CategoryPage() { refetch(); }, [refetch]); - - const handleDrop = (newTreeData: NodeModel[]) => { setTreeData(newTreeData); }; useEffect(() => { - const treeElement = document.querySelector('.tree-container'); + const treeElement = document.querySelector(".tree-container"); if (treeElement) { - expandedIds.forEach(id => { + expandedIds.forEach((id) => { const nodeElement = treeElement.querySelector(`[data-id="${id}"]`); if (nodeElement) { - const toggleButton = nodeElement.querySelector('.toggle-button'); - if (toggleButton && !nodeElement.classList.contains('expanded')) { + const toggleButton = nodeElement.querySelector(".toggle-button"); + if (toggleButton && !nodeElement.classList.contains("expanded")) { (toggleButton as HTMLElement).click(); } } @@ -91,7 +94,7 @@ function CategoryPage() { } }, [expandedIds]); - const handleSearch = (e: React.FormEvent) => { + const handleSearch = (e: React.SubmitEvent) => { e.preventDefault(); if (!searchTerm.trim()) { showAlert("검색어를 입력해주세요"); @@ -99,7 +102,9 @@ function CategoryPage() { } const searchResults = treeData.filter((node) => - (node.text ?? "").toLowerCase().includes((searchTerm ?? "").toLowerCase()) + (node.text ?? "") + .toLowerCase() + .includes((searchTerm ?? "").toLowerCase()), ); if (searchResults.length === 0) { @@ -138,7 +143,7 @@ function CategoryPage() { setNewCategoryName(""); } else { setSelectedSubNode(node); - const parentNode = treeData.find(n => n.id === node.parent) || null; + const parentNode = treeData.find((n) => n.id === node.parent) || null; setSelectedParentNode(parentNode); setNewCategoryName(node.text || ""); @@ -147,7 +152,7 @@ function CategoryPage() { const newExpandedIds = new Set(expandedIds); while (parentId && parentId !== 0 && parentId !== "0") { newExpandedIds.add(parentId); - const parentNode = treeData.find(n => n.id === parentId); + const parentNode = treeData.find((n) => n.id === parentId); if (parentNode) { parentId = parentNode.parent; } else { @@ -230,7 +235,7 @@ function CategoryPage() { showAlert("수정할 분류 구조를 선택해주세요."); return; } - + setLoading(true); try { await CategoryApi.updateCategory({ @@ -283,7 +288,7 @@ function CategoryPage() { showAlert("상위 분류를 선택해주세요."); return; } - + setLoading(true); try { await CategoryApi.createCategory({ @@ -362,7 +367,7 @@ function CategoryPage() { // 백엔드에서 트리 구조로 반환하므로 flat 구조로 변환 const flattenTree = (nodes: any[], parentId = "0"): any[] => { const result: any[] = []; - + nodes.forEach((node) => { // 현재 노드를 flat 구조로 추가 result.push({ @@ -371,65 +376,67 @@ function CategoryPage() { droppable: node.droppAble, text: node.clsfSystmNm, }); - + // 자식 노드들이 있으면 재귀적으로 처리 if (node.children && node.children.length > 0) { result.push(...flattenTree(node.children, node.clsfSystmSn)); } }); - + return result; }; - + const mappedData = flattenTree(categoryList); - + // 트리 데이터를 영문순서, 한글순서로 정렬 (각 레벨별로) const sortTreeData = (nodes: typeof mappedData): typeof mappedData => { // 부모별로 그룹화 const groupedByParent = new Map(); - - nodes.forEach(node => { + + nodes.forEach((node) => { const parent = node.parent; if (!groupedByParent.has(parent)) { groupedByParent.set(parent, []); } groupedByParent.get(parent)!.push(node); }); - + // 각 그룹 내에서 정렬 (최상위 부모부터) - const sortedGroups = Array.from(groupedByParent.entries()).sort(([parentA], [parentB]) => { - // 최상위 부모("0")를 먼저 정렬 - if (parentA === "0" && parentB !== "0") return -1; - if (parentA !== "0" && parentB === "0") return 1; - return 0; - }); - + const sortedGroups = Array.from(groupedByParent.entries()).sort( + ([parentA], [parentB]) => { + // 최상위 부모("0")를 먼저 정렬 + if (parentA === "0" && parentB !== "0") return -1; + if (parentA !== "0" && parentB === "0") return 1; + return 0; + }, + ); + sortedGroups.forEach(([parent, children]) => { children.sort((a, b) => { const textA = a.text || ""; const textB = b.text || ""; - + // 영문이 먼저 오도록 정렬 const isEnglishA = /^[a-zA-Z]/.test(textA); const isEnglishB = /^[a-zA-Z]/.test(textB); - + if (isEnglishA && !isEnglishB) return -1; if (!isEnglishA && isEnglishB) return 1; - + // 같은 언어 내에서는 알파벳/가나다순 정렬 - return textA.localeCompare(textB, ['ko', 'en']); + return textA.localeCompare(textB, ["ko", "en"]); }); }); - + // 정렬된 결과를 다시 합치기 (순서 보장) const sortedData: typeof mappedData = []; sortedGroups.forEach(([parent, children]) => { sortedData.push(...children); }); - + return sortedData; }; - + // 각 레벨별로 정렬 const sortedData = sortTreeData(mappedData); setTreeData(sortedData); @@ -443,8 +450,12 @@ function CategoryPage() { const filterTree = (nodes: NodeModel[]) => { return nodes.reduce((acc: NodeModel[], node) => { - const nodeMatches = (node.text ?? "").toLowerCase().includes((searchTerm ?? "").toLowerCase()); - const filteredChildren = node.droppable ? filterTree(treeData.filter(n => n.parent === node.id)) : []; + const nodeMatches = (node.text ?? "") + .toLowerCase() + .includes((searchTerm ?? "").toLowerCase()); + const filteredChildren = node.droppable + ? filterTree(treeData.filter((n) => n.parent === node.id)) + : []; if (nodeMatches || filteredChildren.length > 0) { acc.push(node); @@ -466,14 +477,16 @@ function CategoryPage() { setAddingParentNode(null); }; - useEffect(() => { - }, [userInfo, userUnqId]); + useEffect(() => {}, [userInfo, userUnqId]); return ( -
+
-
+ @@ -484,50 +497,55 @@ function CategoryPage() { value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> - -
-
-
+
+
-

자료관 분류 트리

+

+ 자료관 분류 트리 +

-
{ // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDrop={(e) => { // 파일 드롭은 방지하되, 트리 내부 드롭은 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragEnter={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragLeave={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } @@ -543,41 +561,50 @@ function CategoryPage() { insertDroppableFirst={false} canDrop={() => false} render={(node, { depth, isOpen, onToggle }) => { - const isExpanded = expandedIds.map(String).includes(String(node.id)); - const isSearchResult = searchTerm && (node.text ?? "").toLowerCase().includes(searchTerm.toLowerCase()); - + const isExpanded = expandedIds + .map(String) + .includes(String(node.id)); + const isSearchResult = + searchTerm && + (node.text ?? "") + .toLowerCase() + .includes(searchTerm.toLowerCase()); + return (
{ + ref={(el) => { if (isSearchResult) { - searchResultRefs.current = [...searchResultRefs.current, el]; + searchResultRefs.current = [ + ...searchResultRefs.current, + el, + ]; } }} data-id={node.id} onDragOver={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDrop={(e) => { // 파일 드롭은 방지하되, 트리 내부 드롭은 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragEnter={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragLeave={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } @@ -586,35 +613,46 @@ function CategoryPage() { handleSelect(node); if (node.droppable) { onToggle(); - setExpandedIds(prev => { + setExpandedIds((prev) => { const idStr = String(node.id); const prevStr = prev.map(String); - const isCurrentlyExpanded = prevStr.includes(idStr); + const isCurrentlyExpanded = + prevStr.includes(idStr); if (isCurrentlyExpanded) { // 닫는 동작: 하위 노드에 선택된 노드가 있으면 선택 해제 // 하위 노드 id 목록 구하기 - const getDescendantIds = (parentId: string | number): (string | number)[] => { - const children = treeData.filter(n => n.parent === parentId); - let all = children.map(n => n.id); - children.forEach(child => { - all = all.concat(getDescendantIds(child.id)); + const getDescendantIds = ( + parentId: string | number, + ): (string | number)[] => { + const children = treeData.filter( + (n) => n.parent === parentId, + ); + let all = children.map((n) => n.id); + children.forEach((child) => { + all = all.concat( + getDescendantIds(child.id), + ); }); return all; }; const descendants = getDescendantIds(node.id); if ( selectedSubNode && - (descendants.includes(selectedSubNode.id) || selectedSubNode.id === node.id) + (descendants.includes(selectedSubNode.id) || + selectedSubNode.id === node.id) ) { setSelectedSubNode(null); setSelectedParentNode(null); setNewCategoryName(""); } - return prevStr.filter(id => id !== idStr); + return prevStr.filter((id) => id !== idStr); } else { // 여는 동작: 자동 선택 setSelectedSubNode(node); - setSelectedParentNode(treeData.find(n => n.id === node.parent) || null); + setSelectedParentNode( + treeData.find((n) => n.id === node.parent) || + null, + ); setNewCategoryName(node.text || ""); return [...prevStr, idStr]; } @@ -622,68 +660,72 @@ function CategoryPage() { } }} style={{ paddingLeft: `${depth * 16}px` }} - className={`cursor-pointer py-3 border-b border-gray-200 flex items-center transition-colors - ${String(selectedSubNode?.id) === String(node.id) + className={`flex cursor-pointer items-center border-b border-gray-200 py-3 transition-colors ${ + String(selectedSubNode?.id) === String(node.id) ? "bg-blue-100" : isSearchResult ? "bg-sky-100" - : "hover:bg-gray-100"} - ${isExpanded ? 'expanded' : ''} - `} + : "hover:bg-gray-100" + } ${isExpanded ? "expanded" : ""} `} > {node.droppable && ( { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDrop={(e) => { // 파일 드롭은 방지하되, 트리 내부 드롭은 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragEnter={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} onDragLeave={(e) => { // 파일 드래그는 방지하되, 트리 내부 드래그는 허용 - if (e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer.types.includes("Files")) { e.preventDefault(); e.stopPropagation(); } }} - onClick={e => { + onClick={(e) => { e.stopPropagation(); onToggle(); - setExpandedIds(prev => { + setExpandedIds((prev) => { const idStr = String(node.id); const prevStr = prev.map(String); - const isCurrentlyExpanded = prevStr.includes(idStr); + const isCurrentlyExpanded = + prevStr.includes(idStr); if (isCurrentlyExpanded) { - return prevStr.filter(id => id !== idStr); + return prevStr.filter((id) => id !== idStr); } else { setSelectedSubNode(node); - setSelectedParentNode(treeData.find(n => n.id === node.parent) || null); + setSelectedParentNode( + treeData.find( + (n) => n.id === node.parent, + ) || null, + ); setNewCategoryName(node.text || ""); return [...prevStr, idStr]; } }); }} - className="mr-2 flex items-center justify-center cursor-pointer toggle-button" + className="toggle-button mr-2 flex cursor-pointer items-center justify-center" > {isExpanded ? ( - + ) : ( - + )} )} @@ -695,36 +737,46 @@ function CategoryPage() {
-
+

분류 추가

-
+
-
-
-
-
+
+
{isAddingMode ? ( <> + + ))} +
-
    - {msg.sources.map((src, i) => ( -
  • - -
  • - ))} -
-
- )} + )}
))} - + {/* 로딩 상태 표시 - 실시간 답변 생성 중에는 표시하지 않음 */} {loading && messages.length === 0 && ( -
- - +
+ + - + 답변을 생성하고 있습니다...
@@ -181,10 +201,10 @@ function ChatBotPage() {
setInput(e.target.value)} @@ -199,4 +219,4 @@ function ChatBotPage() { ); } -export { ChatBotPage }; \ No newline at end of file +export { ChatBotPage }; diff --git a/apps/dm/app/(landing)/reuse/_components/TopNav.tsx b/apps/dm/app/(landing)/reuse/_components/TopNav.tsx index 17c6f5e04..95598aaa7 100644 --- a/apps/dm/app/(landing)/reuse/_components/TopNav.tsx +++ b/apps/dm/app/(landing)/reuse/_components/TopNav.tsx @@ -53,7 +53,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { // ✅ 유효성/서버 에러 표시용 const [errors, setErrors] = useState>>({}); - const [touched, setTouched] = useState>>({}); + const [touched, setTouched] = useState>>( + {}, + ); const [submitting, setSubmitting] = useState(false); useEffect(() => { @@ -99,7 +101,8 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { if (!b) next.bizNo = "사업자등록번호를 입력해주세요."; else if (!isBizNoValid(b)) - next.bizNo = "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)"; + next.bizNo = + "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)"; if (!noWorker && !w) next.workerNo = "종사자등록번호를 입력해주세요."; @@ -115,7 +118,7 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { const markAllTouched = () => setTouched({ bizNo: true, workerNo: true, pw: true }); - const submit = async (e: React.FormEvent) => { + const submit = async (e: React.SubmitEvent) => { e.preventDefault(); markAllTouched(); @@ -138,7 +141,10 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { // --- 데모: 비밀번호가 "1234"면 성공 처리(원하면 제거) --- const serverOk = pw.trim() === "1234"; if (!serverOk) { - setErrors((p) => ({ ...p, pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)" })); + setErrors((p) => ({ + ...p, + pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)", + })); setTouched((p) => ({ ...p, pw: true })); return; } @@ -210,7 +216,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { validate(); }} placeholder="사업자등록번호를 입력하세요. (예: 123-45-67890)" - className={inputCls({ hasError: !!(touched.bizNo && errors.bizNo) })} + className={inputCls({ + hasError: !!(touched.bizNo && errors.bizNo), + })} /> {touched.bizNo && errors.bizNo && (

@@ -251,7 +259,8 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { placeholder="종사자등록번호를 입력하세요." className={inputCls({ disabled: noWorker, - hasError: !noWorker && !!(touched.workerNo && errors.workerNo), + hasError: + !noWorker && !!(touched.workerNo && errors.workerNo), })} /> @@ -279,7 +288,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { validate(); }} placeholder="비밀번호를 입력하세요." - className={inputCls({ hasError: !!(touched.pw && errors.pw) })} + className={inputCls({ + hasError: !!(touched.pw && errors.pw), + })} /> {touched.pw && errors.pw && (

@@ -305,7 +316,9 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { {/* 비밀번호 초기화 요청 */} diff --git a/apps/dm/components/LoginModal.tsx b/apps/dm/components/LoginModal.tsx index 60f46f7ad..21eb4dc65 100644 --- a/apps/dm/components/LoginModal.tsx +++ b/apps/dm/components/LoginModal.tsx @@ -4,7 +4,7 @@ import { useEffect, useId, useState } from "react"; import { useRouter } from "next/navigation"; import { useDmStore } from "@/app/store"; import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form"; -import { sha512 } from 'js-sha512'; +import { sha512 } from "js-sha512"; type LoginModalProps = { open: boolean; @@ -21,13 +21,19 @@ function isBizNoValid(v: string) { } function inputCls(opts: { hasError?: boolean; disabled?: boolean }) { - const base = "h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none"; + const base = + "h-[48px] w-full rounded-[4px] border px-5 text-[18px] outline-none"; if (opts.disabled) return `${base} border-gray-200 bg-gray-50 text-gray-400`; - if (opts.hasError) return `${base} border-red-500 ring-1 ring-red-400 focus:border-red-500`; + if (opts.hasError) + return `${base} border-red-500 ring-1 ring-red-400 focus:border-red-500`; return `${base} border-gray-300 focus:border-sky-400`; } -export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps) { +export default function LoginModal({ + open, + onClose, + onSuccess, +}: LoginModalProps) { const titleId = useId(); const router = useRouter(); const { actionLogin } = useDmStore((state) => state); @@ -39,7 +45,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps // ✅ 유효성/서버 에러 표시용 const [errors, setErrors] = useState>>({}); - const [touched, setTouched] = useState>>({}); + const [touched, setTouched] = useState>>( + {}, + ); const [submitting, setSubmitting] = useState(false); useEffect(() => { @@ -64,7 +72,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps }, [open, onClose]); useEffect(() => { - if (noWorker) { // 종사업자번호 해당없음 + if (noWorker) { + // 종사업자번호 해당없음 setWorkerNo(""); // 종사업자번호 setErrors((p) => { const nextErrors = { ...p }; @@ -122,7 +131,7 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps const markAllTouched = () => setTouched({ bizNo: true, workerNo: true, pw: true }); - const submit = async (e: React.FormEvent) => { + const submit = async (e: React.SubmitEvent) => { e.preventDefault(); markAllTouched(); @@ -150,8 +159,10 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps }; const initPassword = () => { - alert("관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다."); - } + alert( + "관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다.", + ); + }; return (

@@ -212,7 +223,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps validate(); }} placeholder="사업자등록번호를 입력하세요. (예: 123-45-67890)" - className={inputCls({ hasError: !!(touched.bizNo && errors.bizNo) })} + className={inputCls({ + hasError: !!(touched.bizNo && errors.bizNo), + })} /> {touched.bizNo && errors.bizNo && (

@@ -253,7 +266,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps placeholder="종사자등록번호를 입력하세요." className={inputCls({ disabled: noWorker, - hasError: !noWorker && !!(touched.workerNo && errors.workerNo), + hasError: + !noWorker && !!(touched.workerNo && errors.workerNo), })} /> @@ -281,7 +295,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps validate(); }} placeholder="비밀번호를 입력하세요." - className={inputCls({ hasError: !!(touched.pw && errors.pw) })} + className={inputCls({ + hasError: !!(touched.pw && errors.pw), + })} /> {touched.pw && errors.pw && (

diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 44492a1dd..c3d7e4fc2 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import useSWR from 'swr'; +import useSWR from "swr"; import { Button, @@ -12,20 +12,29 @@ import { SelectContent, SelectItem, SelectValue, - Checkbox -} -from "@common/components"; + Checkbox, +} from "@common/components"; import { useAlertStore } from "@/shared/lib"; -import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type"; +import { + type SearchFormProps, + type SearchFormValues, + defaultSearchFormProps, +} from "../type/SearchForm.type"; import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; -import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface"; +import type { + ApiResponse, + SearchCondition, + Sgg, +} from "@/entities/statistics/model/statistics-interface"; const getSidoListFetcher = async (): Promise> => { return statisticsAPi.getSidoList(); }; -const getSggListFetcher = async (key: [string, string]): Promise> => { +const getSggListFetcher = async ( + key: [string, string], +): Promise> => { return statisticsAPi.getSggList(key[1]); }; @@ -46,7 +55,7 @@ const SearchForm = (props: SearchFormProps = {}) => { dedupingInterval: 2000, shouldRetryOnError: false, onSuccess: (data) => { - if (data.status !== 'success') { + if (data.status !== "success") { alert(`시도 목록을 불러오는데 실패했습니다. 새로고침 후 다시 시도해주세요.`); } @@ -55,23 +64,21 @@ const SearchForm = (props: SearchFormProps = {}) => { alert(`시도 목록을 불러오는데 실패했습니다. 새로고침 후 다시 시도해주세요.`); }, - } + }, ); const [values, setValues] = useState({ - sido: props.defaultValues?.sido || '', - gugun: props.defaultValues?.gugun || '', + sido: props.defaultValues?.sido || "", + gugun: props.defaultValues?.gugun || "", year: props.defaultValues?.year || defaultYear.toString(), - order: props.defaultValues?.order || '', - fctName: props.defaultValues?.fctName || '', - bsnmCd: props.defaultValues?.bsnmCd || '', + order: props.defaultValues?.order || "", + fctName: props.defaultValues?.fctName || "", + bsnmCd: props.defaultValues?.bsnmCd || "", isDetail: false, }); const sggKey: [string, string] | null = - values.sido && values.sido !== "ALL" - ? ["sgg-list", values.sido] - : null; + values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null; const { data: sggList } = useSWR>( sggKey, getSggListFetcher, @@ -82,7 +89,7 @@ const SearchForm = (props: SearchFormProps = {}) => { dedupingInterval: 2000, shouldRetryOnError: false, onSuccess: (data) => { - if (data.status !== 'success') { + if (data.status !== "success") { alert(`시군구 목록을 불러오는데 실패했습니다. 새로고침 후 다시 시도해주세요.`); } @@ -91,23 +98,23 @@ const SearchForm = (props: SearchFormProps = {}) => { alert(`시군구 목록을 불러오는데 실패했습니다. 새로고침 후 다시 시도해주세요.`); }, - } + }, ); const sggOptions = sggList?.data ?? []; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); props.onSubmit?.(values); }; const handleReset = () => { const resetValues: SearchFormValues = { - sido: '', - gugun: '', + sido: "", + gugun: "", year: String(defaultYear), - order: '', - fctName: '', - bsnmCd: '', + order: "", + fctName: "", + bsnmCd: "", isDetail: false, }; setValues(resetValues); @@ -115,20 +122,23 @@ const SearchForm = (props: SearchFormProps = {}) => { }; const handleSidoChange = (value: string) => { - const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || ''; + const selectedBsnm = + sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || ""; setValues((prev) => ({ ...prev, sido: value, - gugun: '', + gugun: "", bsnmCd: selectedBsnm, })); }; const handleSggChange = (value: string) => { const listForBsnm = sggList?.data ?? []; - const selectedBsnm = value === 'ALL' - ? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '') - : (listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ''); + const selectedBsnm = + value === "ALL" + ? sidoList?.data.sidoList.find((v) => v.sido === values.sido) + ?.bsnmCd || "" + : listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ""; setValues((prev) => ({ ...prev, @@ -145,66 +155,80 @@ const SearchForm = (props: SearchFormProps = {}) => {

- {mergedProps.sido && + {mergedProps.sido && (
- - 선택 {sidoList?.data?.sidoList?.map((sido) => ( - + {sido.sido} ))}
- } - {mergedProps.gugun && + )} + {mergedProps.gugun && (
- - 전체 {sggOptions?.map((sgg) => ( - + {sgg.gugun} ))}
- } - {mergedProps.year && + )} + {mergedProps.year && (
- - + setValues({ ...values, year: value }) + } + > {sidoList?.data?.yearList.map((year) => ( - + {year}년 ))}
- } - {mergedProps.isDetail && + )} + {mergedProps.isDetail && (
- } - {mergedProps.order && + )} + {mergedProps.order && (
- - + setValues({ ...values, order: value }) + } + > @@ -231,9 +261,14 @@ const SearchForm = (props: SearchFormProps = {}) => {
- } -
-
- {(mergedProps.fctName || mergedProps.userMenual) && + {(mergedProps.fctName || mergedProps.userMenual) && (
- {mergedProps.fctName && + {mergedProps.fctName && (
-
- } - {mergedProps.userMenual && -
-
- } + )}
- } + )}
); }; -export default SearchForm; \ No newline at end of file +export default SearchForm; diff --git a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx index 2994ac7b1..13b436ce4 100644 --- a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx +++ b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx @@ -13,14 +13,18 @@ import { SelectContent, SelectItem, SelectValue, - LoadingWithAnimation + LoadingWithAnimation, } from "@common/components"; import CompactDataGrid from "./CompactDataGrid"; import SaveButton from "../button/SaveButton"; import { useAlertStore } from "@/shared/lib"; import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; -import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface"; +import type { + ApiResponse, + SearchCondition, + Sgg, +} from "@/entities/statistics/model/statistics-interface"; import type { ColumnInfo } from "../type/DataGrid.type"; import type { SeweragePipeFacilitiesListRequest, @@ -31,9 +35,24 @@ import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/inpu /** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */ const treatmentFacilityColumnInfo: ColumnInfo = { columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, - { name: "fctNm", fieldName: "fctNm", header: { text: "시설명" }, editable: false }, + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + editable: false, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + editable: false, + }, + { + name: "fctNm", + fieldName: "fctNm", + header: { text: "시설명" }, + editable: false, + }, { name: "fctCpc", fieldName: "fctCpc", @@ -49,9 +68,24 @@ const treatmentFacilityColumnInfo: ColumnInfo = { /** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */ const adminRegionColumnInfo: ColumnInfo = { columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, - { name: "emdNm", fieldName: "emdNm", header: { text: "읍면동" }, editable: false }, + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + editable: false, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + editable: false, + }, + { + name: "emdNm", + fieldName: "emdNm", + header: { text: "읍면동" }, + editable: false, + }, ], columnLayout: ["ctpvNm", "sggNm", "emdNm"], }; @@ -60,7 +94,9 @@ const getSidoListFetcher = async (): Promise> => { return statisticsAPi.getSidoList(); }; -const getSggAllListFetcher = async (key: [string, string]): Promise> => { +const getSggAllListFetcher = async ( + key: [string, string], +): Promise> => { return statisticsAPi.getSggAllList(key[1]); }; @@ -69,49 +105,72 @@ type SidoSggFacilityNameSearchFormProps = { statsYr?: string; }; -const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFormProps) => { +const SidoSggFacilityNameSearchForm = ({ + statsYr, +}: SidoSggFacilityNameSearchFormProps) => { const { setMessage: alert } = useAlertStore((state) => state); - const { data: sidoList } = useSWR>("sido-list", getSidoListFetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - dedupingInterval: 2000, - shouldRetryOnError: false, - onSuccess: (data) => { - if (data.status !== "success") { - alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`); - } + const { data: sidoList } = useSWR>( + "sido-list", + getSidoListFetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + dedupingInterval: 2000, + shouldRetryOnError: false, + onSuccess: (data) => { + if (data.status !== "success") { + alert( + `시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`, + ); + } + }, + onError: () => { + alert( + `시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`, + ); + }, }, - onError: () => { - alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`); - }, - }); + ); const [sido, setSido] = useState("ALL"); const [gugun, setGugun] = useState("ALL"); const [fctNm, setFctNm] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [treatmentFacilityRows, setTreatmentFacilityRows] = useState[]>([]); - const [adminRegionRows, setAdminRegionRows] = useState[]>([]); + const [treatmentFacilityRows, setTreatmentFacilityRows] = useState< + Record[] + >([]); + const [adminRegionRows, setAdminRegionRows] = useState< + Record[] + >([]); - const sggKey: [string, string] | null = sido && sido !== "ALL" ? ["sgg-all", sido] : null; - const { data: sggList } = useSWR>(sggKey, getSggAllListFetcher, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - dedupingInterval: 2000, - shouldRetryOnError: false, - onSuccess: (data) => { - if (data.status !== "success") { - alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`); - } + const sggKey: [string, string] | null = + sido && sido !== "ALL" ? ["sgg-all", sido] : null; + const { data: sggList } = useSWR>( + sggKey, + getSggAllListFetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + dedupingInterval: 2000, + shouldRetryOnError: false, + onSuccess: (data) => { + if (data.status !== "success") { + alert( + `시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`, + ); + } + }, + onError: () => { + alert( + `시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`, + ); + }, }, - onError: () => { - alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`); - }, - }); + ); const sggOptions = sggList?.data ?? []; /** 시설 목록·행정구역 API 공통 — 하수도 사업자코드(bsnmCd) */ @@ -129,7 +188,7 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor setGugun("ALL"); }; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); if (!statsYr?.trim()) { alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); @@ -153,7 +212,10 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor try { setIsLoading(true); - const response = await statisticsInputSewPipeApiService.getFacilitiesList(params); + const response = + await statisticsInputSewPipeApiService.getFacilitiesList( + params, + ); if (response.status !== "success") { alert(response.message ?? "처리시설 목록 조회에 실패했습니다."); return; @@ -178,7 +240,9 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor } }; - const handleTreatmentFacilityRowClick = async (row: Record) => { + const handleTreatmentFacilityRowClick = async ( + row: Record, + ) => { if (!statsYr?.trim()) { alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); return; @@ -197,14 +261,17 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor const params: SeweragePipeFacilityAdminAreaListRequest = { statsYr: statsYr.trim(), swtrmtFcltCd, - sewerBzmnCd: String(row.admdstCd ?? "").trim(), + sewerBzmnCd: String(row.admdstCd ?? "").trim(), ctpvNm, sggNm, }; try { setIsLoading(true); - const response = await statisticsInputSewPipeApiService.getFacilityAdminAreaList(params); + const response = + await statisticsInputSewPipeApiService.getFacilityAdminAreaList( + params, + ); if (response.status !== "success") { alert(response.message ?? "행정구역 목록 조회에 실패했습니다."); return; @@ -234,7 +301,7 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
- +
- - 전체 {sggOptions?.map((sgg) => ( - + {sgg.gugun} ))}
-
- +
+ setFctNm(e.target.value)} placeholder="시설명 입력" />
-
@@ -282,14 +363,18 @@ const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFor
- {isLoading &&
-
- + {isLoading && ( +
+
+ +
-
} -
-
-

처리시설 정보

+ )} +
+
+

+ 처리시설 정보 +

-
+
-

행정구역 정보

+

+ 행정구역 정보 +

{ // const { setMessage: alert } = useAlertStore((state) => state); - const [statsCd, setStatsCd] = useState(''); + const [statsCd, setStatsCd] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [rowData, setRowData] = useState[]>([]); + const [rowData, setRowData] = useState[]>( + [], + ); const [pageIndex, setPageIndex] = useState(0); const [totalPages, setTotalPages] = useState(0); - const loadLogs = useCallback(async (page1Based: number) => { - const param: StatisticsLogRequest = { - statsCd: (statsCd === '' || statsCd === 'ALL') ? '' : statsCd, - page: page1Based, - size: PAGE_SIZE, - }; + const loadLogs = useCallback( + async (page1Based: number) => { + const param: StatisticsLogRequest = { + statsCd: statsCd === "" || statsCd === "ALL" ? "" : statsCd, + page: page1Based, + size: PAGE_SIZE, + }; - try { - setIsLoading(true); + try { + setIsLoading(true); - const response = await statisticsAPi.getLogList(param); + const response = await statisticsAPi.getLogList(param); - if (response.status === 'success') { - setRowData(toGridRows(response.data?.statsLogList ?? [])); - setTotalPages(response.data?.pageInfo?.totalPages ?? 0); - setPageIndex(Math.max(0, page1Based - 1)); - } else { - alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); + if (response.status === "success") { + setRowData(toGridRows(response.data?.statsLogList ?? [])); + setTotalPages(response.data?.pageInfo?.totalPages ?? 0); + setPageIndex(Math.max(0, page1Based - 1)); + } else { + alert( + response.message || + "데이터 조회 중 오류가 발생했습니다.", + ); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); - } - }, [statsCd]); + }, + [statsCd], + ); - const handleSearch = async (e: React.FormEvent) => { + const handleSearch = async (e: React.SubmitEvent) => { e.preventDefault(); await loadLogs(1); }; @@ -65,9 +68,7 @@ const LogHistory = () => {
- + { className="w-[16rem]" />
-
- -
- {isLoading &&
-
- + {isLoading && ( +
+
+ +
-
} + )} { }} + setRowData={() => {}} /> { ); }; -export default LogHistory; \ No newline at end of file +export default LogHistory; diff --git a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx index c91b520ce..99b8391ed 100644 --- a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx +++ b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx @@ -211,7 +211,7 @@ function VerificationArticleManagement() { setDialogOpen(true); } - function handleSubmit(e: React.FormEvent) { + function handleSubmit(e: React.SubmitEvent) { e.preventDefault(); handleSearch(); } diff --git a/apps/st/src/pages/auth/login/ui/login-page.tsx b/apps/st/src/pages/auth/login/ui/login-page.tsx index 8d2b3300a..9f42d1423 100644 --- a/apps/st/src/pages/auth/login/ui/login-page.tsx +++ b/apps/st/src/pages/auth/login/ui/login-page.tsx @@ -6,124 +6,132 @@ import { LoginApi } from "@/entities/auth"; import "./login.css"; function LoginPage() { - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e: React.SubmitEvent) => { + e.preventDefault(); - const formData = new FormData(e.target as HTMLFormElement); - const userId = formData.get("id") as string; - const password = formData.get("password") as string; + const formData = new FormData(e.target as HTMLFormElement); + const userId = formData.get("id") as string; + const password = formData.get("password") as string; - await LoginApi.login({ userId, password }); - window.location.href = "/statistics"; - }; + await LoginApi.login({ userId, password }); + window.location.href = "/statistics"; + }; - return ( -
-
-
-

- 유역하수도통합플랫폼 -
- 하수도통계 관리시스템 -

-

유역하수도통합플랫폼 하수도통계 관리시스템에 오신 것을 환영합니다.

-
-
- logo -
-
-
-
-
    -
  • -

    ID

    -
    - - -
    -
  • -
  • -

    PASSWORD

    -
    - - -
    -
  • -
-
-
- - + return ( +
+
+
+

+ 유역하수도통합플랫폼 +
+ 하수도통계 관리시스템 +

+

+ 유역하수도통합플랫폼 하수도통계 관리시스템에 오신 것을 + 환영합니다. +

+
+
+ logo +
- {/* 비밀번호 또는 아이디를 잃어버리셨나요? */} -
- -
-
-

copyright © 2023 KECO. All rights reserved.

-
- -

- - 주소 : (22689) 인천광역시 서구 환경로 42(경서동 - 종합환경연구단지) - - - 전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723, - 3722 - - 팩스 :032-590-3729 -

-
+ +
+
    +
  • +

    ID

    +
    + + +
    +
  • +
  • +

    PASSWORD

    +
    + + +
    +
  • +
+
+
+ + +
+ {/* 비밀번호 또는 아이디를 잃어버리셨나요? */} +
+ +
+
+

copyright © 2023 KECO. All rights reserved.

+
+ +

+ + 주소 : (22689) 인천광역시 서구 환경로 42(경서동 + 종합환경연구단지) + + + 전화 : (시스템) 032-590-4321, 3752, 4314 / (통 + 계) 032-590-3723, 3722 + + 팩스 :032-590-3729 +

+
+
+
- -
- ); + ); } export { LoginPage }; diff --git a/apps/wr/components/lib/func/TableData.tsx b/apps/wr/components/lib/func/TableData.tsx index 89d347c24..1bef79403 100644 --- a/apps/wr/components/lib/func/TableData.tsx +++ b/apps/wr/components/lib/func/TableData.tsx @@ -2,18 +2,13 @@ import { numSize } from "@/app/store/common/code/basicCode"; import type { TableUpperProps } from "@/app/store/common/type/header"; import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn"; -import { - Checkbox, - Input, - TableCell, - TableRow, -} from "@common/components/ui"; +import { Checkbox, Input, TableCell, TableRow } from "@common/components/ui"; import moment from "moment"; import { codeToText, formatCommaNumber, formatYyyymmdd, - handleCommaNumberChange + handleCommaNumberChange, } from "./common"; export const rainChkTableData = (chkData: rainChkZodType[]) => { @@ -73,10 +68,10 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => { root.rswtrDetails.forEach((detail) => { let flatRswtChk = {}; const findGuide = root.guideDetails.find( - (guide) => guide.utztnUsgCd === detail.utztnUsgCd - ) + (guide) => guide.utztnUsgCd === detail.utztnUsgCd, + ); const findPoluttion = root.rswtrPollutions.find( - (polution) => polution.utztnUsgCd === detail.utztnUsgCd + (polution) => polution.utztnUsgCd === detail.utztnUsgCd, ); flatRswtChk = { ...root, @@ -84,7 +79,7 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => { ...findGuide, ...findPoluttion, ...flatRswtChk, - utztnUsgCdText: codeToText(detail.utztnUsgCd, 'dmdUszCd'), + utztnUsgCdText: codeToText(detail.utztnUsgCd, "dmdUszCd"), }; makeRswtData.push(flatRswtChk); }); @@ -101,12 +96,12 @@ export const chRowspanTable = ( handleChange: ( rowIndex: number, field: keyof (typeof makeTableData)[0], - value: string | number + value: string | number, ) => void, toggleSelection: (pk: number, checked: boolean) => void, selectedIds: number[], handleCellClick: (index: number, headId: string) => void, - disable?: boolean + disable?: boolean, ) => { let order = 0; // rowspan된 셀 렌더링 여부 추적용 @@ -140,7 +135,11 @@ export const chRowspanTable = ( key={key} rowSpan={rowspan} className="align-middle text-center whitespace-normal break-words" - style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }} + style={{ + width: head.widthPercent + ? `${head.widthPercent}%` + : undefined, + }} > {key === "checkBox" ? ( {formatYyyymmdd(value)} - ) : - ( - <>{value} - )} + formatCommaNumber(value) + ) : key === "chckYmd" ? ( + <>{formatYyyymmdd(value)} + ) : ( + <>{value} + )} ); } @@ -173,7 +172,11 @@ export const chRowspanTable = ( key={key} className="text-center whitespace-normal break-words" onClick={() => handleCellClick(rowIndex, head.id)} - style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }} + style={{ + width: head.widthPercent + ? `${head.widthPercent}%` + : undefined, + }} > {head.edit === true ? ( [ @@ -187,15 +190,18 @@ export const chRowspanTable = ( type={head.type} onChange={(e) => { handleCommaNumberChange(e, (val) => - handleChange(rowIndex, head.id, val) + handleChange(rowIndex, head.id, val), ); }} value={formatCommaNumber(value) || ""} disabled={!selectedIds.includes(row.chkgdnSn)} maxLength={numSize} - onInput={(e: React.FormEvent) => { + onInput={(e: React.SubmitEvent) => { const el = e.currentTarget; - if (head.type === "number" && el.value.length > numSize) { + if ( + head.type === "number" && + el.value.length > numSize + ) { el.value = el.value.slice(0, numSize); } }} @@ -226,9 +232,12 @@ export const chRowspanTable = ( value={value || ""} disabled={!selectedIds.includes(row.chkgdnSn)} maxLength={numSize} - onInput={(e: React.FormEvent) => { + onInput={(e: React.SubmitEvent) => { const el = e.currentTarget; - if (head.type === "number" && el.value.length > numSize) { + if ( + head.type === "number" && + el.value.length > numSize + ) { el.value = el.value.slice(0, numSize); } }} @@ -237,12 +246,12 @@ export const chRowspanTable = ( ) : ["smll"].includes(head.id) && head.type === "text" ? ( <>- ) : [ - "totalSplyUswtrUtztnQnt", - "totalRuseFcltUtztnQnt", - "splyUswtrUtztnQnt", - "ruseFcltUtztnQnt", - "levyAmt", - ].includes(head.id) ? ( + "totalSplyUswtrUtztnQnt", + "totalRuseFcltUtztnQnt", + "splyUswtrUtztnQnt", + "ruseFcltUtztnQnt", + "levyAmt", + ].includes(head.id) ? ( { return headList.forEach((head) => { if (head.edit !== undefined) head.edit = isEdit; @@ -295,7 +304,7 @@ const handleChange = ( rowIndex: number, field: any, value: string, - setInputData: (data: any) => void + setInputData: (data: any) => void, ) => { if (Number(value) < 0 || !Number(value)) { value = "0"; @@ -309,17 +318,16 @@ const handleChange = ( }; } return row; - }) + }), ); }; - // 재이용실적등록 > 지도점검결과 등록 - 상세 modal의 body export const InputTableBody = ( chgRes: any[] | [], headList: TableUpperProps[], setInputData: (data: any[]) => void, - isEditable: boolean = true + isEditable: boolean = true, ) => { // mergeCell 처리용 const getRowSpan = (data: any[], key: string, rowIndex: number) => { @@ -360,13 +368,17 @@ export const InputTableBody = ( // // ); // } - const isTextColumn = head.id === "구분" || head.id === "utztnUsgCdText"; + const isTextColumn = + head.id === "구분" || head.id === "utztnUsgCdText"; const textAlignClass = isTextColumn ? "text-center" : "text-right"; // 총합계 행이면 "구분"에서 colSpan=2로 병합 - if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "구분" ) { + if ( + (data["구분"] === 0 || data["용수명"] === 0) && + head.id === "구분" + ) { return ( void, - isEditable: boolean = true + isEditable: boolean = true, ) => { // mergeCell 처리용 const getRowSpan = (data: any[], key: string, rowIndex: number) => { @@ -584,8 +599,8 @@ export const pbnstTableBody = ( dataIndex, head.id, String(val), - setInputData - ) + setInputData, + ), ) } value={formatCommaNumber(data[head.id as keyof any]) ?? ""} @@ -626,4 +641,4 @@ export const pbnstTableBody = ( )} ); -}; \ No newline at end of file +}; diff --git a/apps/wr/src/features/auth/login/ui/SignupModal.tsx b/apps/wr/src/features/auth/login/ui/SignupModal.tsx index 00a244e13..2be88cce9 100644 --- a/apps/wr/src/features/auth/login/ui/SignupModal.tsx +++ b/apps/wr/src/features/auth/login/ui/SignupModal.tsx @@ -14,7 +14,9 @@ interface SignupModalProps { } function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - const [currentStep, setCurrentStep] = useState<'agreement' | 'signup'>('agreement'); + const [currentStep, setCurrentStep] = useState<"agreement" | "signup">( + "agreement", + ); const [agreed, setAgreed] = useState(false); const [formData, setFormData] = useState({ userId: "", @@ -47,7 +49,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { // 모달이 닫힐 때 초기화 useEffect(() => { if (!open) { - setCurrentStep('agreement'); + setCurrentStep("agreement"); setAgreed(false); setFormData({ userId: "", @@ -81,17 +83,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const handleNextStep = () => { if (agreed) { - setCurrentStep('signup'); + setCurrentStep("signup"); } }; - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = ( + e: React.ChangeEvent, + ) => { const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - + setFormData((prev) => ({ ...prev, [name]: value })); + // 에러 초기화 if (errors[name]) { - setErrors(prev => ({ ...prev, [name]: "" })); + setErrors((prev) => ({ ...prev, [name]: "" })); } }; @@ -102,20 +106,27 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { if (!formData.userId) newErrors.userId = "아이디를 입력해주세요."; if (!formData.userName) newErrors.userName = "이름을 입력해주세요."; if (!formData.password) newErrors.password = "비밀번호를 입력해주세요."; - if (!formData.confirmPassword) newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; + if (!formData.confirmPassword) + newErrors.confirmPassword = "비밀번호 확인을 입력해주세요."; if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요."; if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요."; if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요."; // 비밀번호 확인 - if (formData.password && formData.confirmPassword && formData.password !== formData.confirmPassword) { + if ( + formData.password && + formData.confirmPassword && + formData.password !== formData.confirmPassword + ) { newErrors.confirmPassword = "비밀번호가 일치하지 않습니다."; } // 비밀번호 정규식 검증 - const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; + const passwordRegex = + /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/; if (formData.password && !passwordRegex.test(formData.password)) { - newErrors.password = "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; + newErrors.password = + "비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다."; } // 이메일 형식 검증 @@ -124,8 +135,10 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { } // 중복 체크 검증 - if (idAvailable === false) newErrors.userId = "이미 사용 중인 아이디입니다."; - if (emailAvailable === false) newErrors.email = "이미 사용 중인 이메일입니다."; + if (idAvailable === false) + newErrors.userId = "이미 사용 중인 아이디입니다."; + if (emailAvailable === false) + newErrors.email = "이미 사용 중인 이메일입니다."; setErrors(newErrors); const isValid = Object.keys(newErrors).length === 0; @@ -135,7 +148,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const checkUserIdDuplicate = async () => { if (!formData.userId) { - setErrors(prev => ({ ...prev, userId: "아이디를 입력해주세요." })); + setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." })); return; } @@ -147,19 +160,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, - { userId: formData.userId } + { userId: formData.userId }, ); const exists = response.data; setIdAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, userId: "이미 사용 중인 아이디입니다." })); + setErrors((prev) => ({ + ...prev, + userId: "이미 사용 중인 아이디입니다.", + })); } else { - setErrors(prev => ({ ...prev, userId: "" })); + setErrors((prev) => ({ ...prev, userId: "" })); } } catch (error) { console.error("아이디 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, userId: "아이디 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + userId: "아이디 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingId(false); } @@ -167,7 +186,7 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { const checkEmailDuplicate = async () => { if (!formData.email) { - setErrors(prev => ({ ...prev, email: "이메일을 입력해주세요." })); + setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." })); return; } @@ -179,28 +198,34 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/exists`, - { email: formData.email } + { email: formData.email }, ); const exists = response.data; setEmailAvailable(!exists); if (exists) { - setErrors(prev => ({ ...prev, email: "이미 사용 중인 이메일입니다." })); + setErrors((prev) => ({ + ...prev, + email: "이미 사용 중인 이메일입니다.", + })); } else { - setErrors(prev => ({ ...prev, email: "" })); + setErrors((prev) => ({ ...prev, email: "" })); } } catch (error) { console.error("이메일 중복 체크 오류:", error); - setErrors(prev => ({ ...prev, email: "이메일 중복 체크 중 오류가 발생했습니다." })); + setErrors((prev) => ({ + ...prev, + email: "이메일 중복 체크 중 오류가 발생했습니다.", + })); } finally { setIsCheckingEmail(false); } }; - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); console.log("회원가입 버튼 클릭됨"); - + if (!validateForm()) { console.log("폼 검증 실패"); return; @@ -230,19 +255,19 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { // SignupClient를 통해 익명사용자 회원가입 API 호출 const signupClient = SignupClient.getInstance({ - baseURL: BASE_URL + baseURL: BASE_URL, }); const response = await signupClient.post( `${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`, - signupData + signupData, ); console.log("회원가입 성공:", response.data); - + // 성공 시 모달 닫기 및 콜백 호출 onSuccess(); onClose(); - + // 폼 초기화 setFormData({ userId: "", @@ -267,13 +292,12 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { setErrors({}); setIdAvailable(null); setEmailAvailable(null); - } catch (error: unknown) { console.error("회원가입 오류:", error); if (error instanceof Error) { - setErrors(prev => ({ ...prev, submit: error.message })); + setErrors((prev) => ({ ...prev, submit: error.message })); } else { - setErrors(prev => ({ ...prev, submit: "회원가입에 실패했습니다." })); + setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." })); } } finally { setIsLoading(false); @@ -286,36 +310,52 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
e.stopPropagation()}>
-

{currentStep === 'agreement' ? '개인정보제공동의' : '회원가입'}

- +

+ {currentStep === "agreement" ? "개인정보제공동의" : "회원가입"} +

+
- - {currentStep === 'agreement' ? ( + + {currentStep === "agreement" ? (

개인정보 수집 및 이용 동의

회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.

- +

1. 수집하는 개인정보 항목

    -
  • 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, 기관구분
  • -
  • 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, 상세기관코드, 사업자등록번호
  • +
  • + 필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분, + 기관구분 +
  • +
  • + 선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드, + 상세기관코드, 사업자등록번호 +
- +

2. 개인정보의 수집 및 이용목적

  • 회원가입 및 관리
  • 서비스 제공 및 운영
  • 고객상담 및 문의응답
- +

3. 개인정보의 보유 및 이용기간

-

회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 기간까지)

- +

+ 회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당 + 기간까지) +

+

4. 동의 거부권 및 거부 시 불이익

-

개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 시 회원가입이 제한됩니다.

+

+ 개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부 + 시 회원가입이 제한됩니다. +

- +
- +
-
- {idAvailable === true && 사용 가능한 아이디입니다.} - {errors.userId && {errors.userId}} + {idAvailable === true && ( + + 사용 가능한 아이디입니다. + + )} + {errors.userId && ( + {errors.userId} + )}
@@ -390,7 +432,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { placeholder="이름을 입력하세요" disabled={isLoading} /> - {errors.userName && {errors.userName}} + {errors.userName && ( + {errors.userName} + )}
@@ -416,8 +460,14 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { {isCheckingEmail ? "확인중..." : "중복확인"}
- {emailAvailable === true && 사용 가능한 이메일입니다.} - {errors.email && {errors.email}} + {emailAvailable === true && ( + + 사용 가능한 이메일입니다. + + )} + {errors.email && ( + {errors.email} + )}
@@ -434,7 +484,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.password && {errors.password}} + {errors.password && ( + {errors.password} + )}
@@ -451,7 +503,11 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={20} disabled={isLoading} /> - {errors.confirmPassword && {errors.confirmPassword}} + {errors.confirmPassword && ( + + {errors.confirmPassword} + + )}
@@ -468,7 +524,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.pswdHnt && {errors.pswdHnt}} + {errors.pswdHnt && ( + {errors.pswdHnt} + )}
@@ -485,14 +543,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={100} disabled={isLoading} /> - {errors.pswdAns && {errors.pswdAns}} + {errors.pswdAns && ( + {errors.pswdAns} + )}

연락처 정보

- +
@@ -506,7 +566,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.mphn && {errors.mphn}} + {errors.mphn && ( + {errors.mphn} + )}
@@ -523,14 +585,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={11} disabled={isLoading} /> - {errors.telno && {errors.telno}} + {errors.telno && ( + {errors.telno} + )}

주소 정보

- +
@@ -544,7 +608,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={5} disabled={isLoading} /> - {errors.zip && {errors.zip}} + {errors.zip && ( + {errors.zip} + )}
@@ -561,7 +627,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.addr && {errors.addr}} + {errors.addr && ( + {errors.addr} + )}
@@ -578,14 +646,16 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={200} disabled={isLoading} /> - {errors.daddr && {errors.daddr}} + {errors.daddr && ( + {errors.daddr} + )}

기관 정보

- +
@@ -601,7 +671,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.laeSeCd && {errors.laeSeCd}} + {errors.laeSeCd && ( + {errors.laeSeCd} + )}
@@ -621,7 +693,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { - {errors.ogdpSeCd && {errors.ogdpSeCd}} + {errors.ogdpSeCd && ( + {errors.ogdpSeCd} + )}
@@ -638,7 +712,9 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.instSeCd && {errors.instSeCd}} + {errors.instSeCd && ( + {errors.instSeCd} + )}
@@ -655,21 +731,25 @@ function SignupModal({ open, onClose, onSuccess }: SignupModalProps) { maxLength={10} disabled={isLoading} /> - {errors.dtlInstCd && {errors.dtlInstCd}} + {errors.dtlInstCd && ( + {errors.dtlInstCd} + )}
{errors.submit && (
-
{errors.submit}
+
+ {errors.submit} +
)}