Merge branch 'iipbs/as/dev'
This commit is contained in:
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
{
|
||||
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요.
|
||||
// 기존 특성에 대한 설명을 보려면 가리킵니다.
|
||||
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요.
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:as 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:as"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:cs 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:cs"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "dev:cs 서버 연결 (attach)",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/apps/cs/**",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "dev:cs 클라이언트 디버그 (Chrome)",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/apps/cs",
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "dev:st 디버그",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev:st"
|
||||
],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"sourceMaps": true,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pnpm": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CnfmModifyPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default async function InstlSncnModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <CnfmModifyPage id={id} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CnfmViewPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default async function InstlSncnViewPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <CnfmViewPage id={id} />;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { CnfmListPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default CnfmListPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { CnfmRegistPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default CnfmRegistPage;
|
||||
@@ -80,11 +80,11 @@ BEGIN
|
||||
// 영역 B: 행 추가 (인라인 신규 행)
|
||||
ON [행 추가] 버튼 CLICK DO
|
||||
VALIDATE 서식 선택됨
|
||||
VALIDATE 편집 중인 행 없음 (editingRowId == null)
|
||||
VALIDATE 편집 중인 행 없음 (editingRowKey == null)
|
||||
ADD 영역 B-2 그리드 맨 아래 편집 가능 행
|
||||
SET editingRowId = 신규 행 임시 키
|
||||
SET editingRowKey = rowKey (draft-{timestamp})
|
||||
SET 편집상태 = 신규
|
||||
SET aplnId = 클라이언트 임시 ID (저장 시 서버 등록용 ID로 전송)
|
||||
SET aplnId = 미설정 (POST 저장 시 서버 fn_gen_apln_id 발급)
|
||||
SET atrzSeqNo = 현재 행 수 + 1
|
||||
SET atrzSeComCd, aprvrUuid = 빈 값
|
||||
DISABLE 나머지 행 편집 · [행 추가] · 드래그 · [결재선 저장](정책에 따라)
|
||||
@@ -182,13 +182,13 @@ END
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | -------------- | ------ | ---------------------------------------- |
|
||||
| 전자결재서식아이디 | `elaprTmpltId` | string | 등록 시 클라이언트 또는 정책에 따라 발급 |
|
||||
| 결재서식명 | `elaprTmpltNm` | string | |
|
||||
| 문서구분 | `docSeComNm` | string | 코드 `docSeComCd`, 상위코드 `ASDOCSECD` |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** 버튼 |
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | -------------- | ------ | --------------------------------------- |
|
||||
| 전자결재서식아이디 | `elaprTmpltId` | string | 등록 시 서버(DB 함수) 자동 발급 |
|
||||
| 결재서식명 | `elaprTmpltNm` | string | |
|
||||
| 문서구분 | `docSeComNm` | string | 코드 `docSeComCd`, 상위코드 `ASDOCSECD` |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** 버튼 |
|
||||
|
||||
- 행 클릭 시: `elaprTmpltId` 선택 → 영역 B 라벨·B-2 그리드 갱신.
|
||||
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`).
|
||||
@@ -203,11 +203,11 @@ END
|
||||
|
||||
### 4-4. 다이얼로그(개요)
|
||||
|
||||
| 다이얼로그명 | 트리거 | 입력 항목 | 비고 |
|
||||
| ------------------ | ----------- | ----------------------------------------------- | ---------------------- |
|
||||
| 결재서식 등록 | [서식 등록] | `elaprTmpltId`, `elaprTmpltNm`, `docSeComCd` | `elaprTmpltExpln` 선택 |
|
||||
| 결재서식 수정 | 행별 [수정] | `elaprTmpltNm`, `docSeComCd`, `elaprTmpltExpln` | ID는 변경 불가 |
|
||||
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
|
||||
| 다이얼로그명 | 트리거 | 입력 항목 | 비고 |
|
||||
| ------------------ | ----------- | ----------------------------------------------- | ----------------------------------------- |
|
||||
| 결재서식 등록 | [서식 등록] | `elaprTmpltNm`, `docSeComCd` | `elaprTmpltExpln` 선택, ID는 저장 후 응답 |
|
||||
| 결재서식 수정 | 행별 [수정] | `elaprTmpltNm`, `docSeComCd`, `elaprTmpltExpln` | ID는 변경 불가 |
|
||||
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
|
||||
|
||||
---
|
||||
|
||||
@@ -230,7 +230,7 @@ END
|
||||
| 컬럼명 | Field | 입력 UI (편집 모드) | 비고 |
|
||||
| ------------- | ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| (드래그 핸들) | — | — | `useSortable` 연동 |
|
||||
| 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: 임시 ID 표시, 저장 후 서버 ID |
|
||||
| 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: `—`, 저장 후 서버 `aplnId` 표시 |
|
||||
| 결재순서번호 | `atrzSeqNo` | 숫자 Input | 드래그 시 자동 재부여 가능 |
|
||||
| 결재구분 | `atrzSeComCd` | Select (`ATRZSECD` 공통코드) | 표시명 `atrzSeComNm` (조회 행) |
|
||||
| 결재자 | `aprvrUuid` | 읽기 전용 이름 + **[선택]** 버튼 | 응답에 성명 없음 — 선택 시 로컬 `aprvrNm` 유지 |
|
||||
@@ -303,7 +303,7 @@ END
|
||||
| `docSeComNm` | string? | 문서구분명 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**ElaprTmpltSaveRequest**: `elaprTmpltId`_, `elaprTmpltNm`_, `elaprTmpltExpln?`, `docSeComCd?`
|
||||
**ElaprTmpltSaveRequest**: `elaprTmpltNm`\_, `elaprTmpltExpln?`, `docSeComCd?`
|
||||
|
||||
**ElaprTmpltUpdateRequest**: `elaprTmpltNm`\*, `elaprTmpltExpln?`, `docSeComCd?`
|
||||
|
||||
@@ -333,7 +333,7 @@ END
|
||||
| `atrzSeqNo` | number? | 결재순서번호 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**ElaprAplnSaveRequest**: `aplnId`_, `elaprTmpltId`_, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
|
||||
**ElaprAplnSaveRequest**: `elaprTmpltId`\_, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
|
||||
|
||||
**ElaprAplnUpdateRequest**: `elaprTmpltId`\*, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
|
||||
|
||||
@@ -398,7 +398,6 @@ END
|
||||
### 7-5. 미확정 / 검증 필요
|
||||
|
||||
- 검토인력 API 경로·DTO.
|
||||
- `aplnId` / `elaprTmpltId` 발급 정책(클라이언트 생성 규칙).
|
||||
- `@dnd-kit` + react-table 행 인라인 Input 동시 적용 POC.
|
||||
- 결재자 성명 표시: `aprvrUuid` → 사용자명 조회 API.
|
||||
- 영역 A·B 페이지네이션·검색 UI 노출 여부.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
|
||||
|
||||
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
|
||||
const serverApiUrl = process.env.SERVER_API_URL;
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 3003",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p 3003",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger ."
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ export interface ElaprAplnListParams extends PageableParams {
|
||||
}
|
||||
|
||||
export interface ElaprAplnSaveRequest {
|
||||
aplnId: string;
|
||||
elaprTmpltId: string;
|
||||
aprvrUuid?: string;
|
||||
atrzSeComCd?: string;
|
||||
|
||||
@@ -20,7 +20,6 @@ export interface ElaprTmpltSearchParams extends PageableParams {
|
||||
}
|
||||
|
||||
export interface ElaprTmpltSaveRequest {
|
||||
elaprTmpltId: string;
|
||||
elaprTmpltNm: string;
|
||||
elaprTmpltExpln?: string;
|
||||
docSeComCd?: string;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
InstlSncnMng,
|
||||
InstlSncnMngSearchParams,
|
||||
InstlSncnMngService,
|
||||
SpringPage,
|
||||
} from "../model/instl-sncn-interface";
|
||||
|
||||
export class InstlSncnApiService implements InstlSncnMngService {
|
||||
public async getPage(params: InstlSncnMngSearchParams): Promise<SpringPage<InstlSncnMng>> {
|
||||
const response = await client.get<SpringPage<InstlSncnMng>>(ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getById(id: string): Promise<InstlSncnMng> {
|
||||
const response = await client.get<InstlSncnMng>(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const InstlSncnApi = new InstlSncnApiService();
|
||||
@@ -0,0 +1,6 @@
|
||||
export { InstlSncnApi } from "./api/instl-sncn-service";
|
||||
export type {
|
||||
InstlSncnMng,
|
||||
InstlSncnMngSearchParams,
|
||||
SpringPage,
|
||||
} from "./model/instl-sncn-interface";
|
||||
@@ -0,0 +1,58 @@
|
||||
export interface SpringPage<T> {
|
||||
content: T[];
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
number: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
numberOfElements: number;
|
||||
}
|
||||
|
||||
export interface InstlSncnMngSearchParams {
|
||||
bizNm?: string;
|
||||
taskSeComCd?: string;
|
||||
sidoComCd?: string;
|
||||
sggComCd?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export interface InstlSncnMng {
|
||||
instlSncnDocNo: string;
|
||||
dongId: string;
|
||||
sidoNm?: string;
|
||||
sggNm?: string;
|
||||
chrgrNm?: string;
|
||||
instNm?: string;
|
||||
envoSeComCd: string;
|
||||
envoSeCdNm: string;
|
||||
taskSeComCd: string;
|
||||
taskSeCdNm: string;
|
||||
bgtBizCd: string;
|
||||
bgtAplyYr: string;
|
||||
bizNm: string;
|
||||
bizBgngYr: string;
|
||||
bizEndYr: string;
|
||||
bizCn: string;
|
||||
aplcntNm: string;
|
||||
aplcntMblTelno: string;
|
||||
aplcntOgdpNm: string;
|
||||
aplcntEmlAddr: string;
|
||||
dsgnrNm: string;
|
||||
dsgnrTelno: string;
|
||||
dsgnrOgdpNm: string;
|
||||
dsgnrEmlAddr: string;
|
||||
atchFileGroupId: string;
|
||||
prgrsSttsComCd: string;
|
||||
prgrsSttsCdNm: string;
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtrNm: string;
|
||||
}
|
||||
|
||||
export interface InstlSncnMngService {
|
||||
getPage: (params: InstlSncnMngSearchParams) => Promise<SpringPage<InstlSncnMng>>;
|
||||
getById: (id: string) => Promise<InstlSncnMng>;
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import {
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
} from "@common/components";
|
||||
|
||||
export interface ElaprTmpltFormValues {
|
||||
elaprTmpltId: string;
|
||||
elaprTmpltId?: string;
|
||||
elaprTmpltNm: string;
|
||||
docSeComCd: string;
|
||||
elaprTmpltExpln: string;
|
||||
@@ -44,7 +43,6 @@ function ElaprTmpltFormDialog({
|
||||
}: ElaprTmpltFormDialogProps) {
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm<ElaprTmpltFormValues>({
|
||||
defaultValues: {
|
||||
elaprTmpltId: "",
|
||||
elaprTmpltNm: "",
|
||||
docSeComCd: "",
|
||||
elaprTmpltExpln: "",
|
||||
@@ -67,8 +65,6 @@ function ElaprTmpltFormDialog({
|
||||
}
|
||||
|
||||
reset({
|
||||
// TBD: elaprTmpltId 발급 정책 — 현재 클라이언트 임시 ID
|
||||
elaprTmpltId: generateClientId("TMPL"),
|
||||
elaprTmpltNm: "",
|
||||
docSeComCd: "",
|
||||
elaprTmpltExpln: "",
|
||||
@@ -82,10 +78,12 @@ function ElaprTmpltFormDialog({
|
||||
<DialogTitle>{mode === "create" ? "결재서식 등록" : "결재서식 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltId">전자결재서식아이디</Label>
|
||||
<Input id="elaprTmpltId" disabled {...register("elaprTmpltId", { required: true })} />
|
||||
</div>
|
||||
{mode === "edit" ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltId">전자결재서식아이디</Label>
|
||||
<Input id="elaprTmpltId" disabled {...register("elaprTmpltId")} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltNm">결재서식명</Label>
|
||||
<Input id="elaprTmpltNm" {...register("elaprTmpltNm", { required: true })} />
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CnfmListPage } from "./ui/cnfm-list-page";
|
||||
export { CnfmViewPage } from "./ui/cnfm-view-page";
|
||||
export { CnfmRegistPage } from "./ui/cnfm-regist-page";
|
||||
export { CnfmModifyPage } from "./ui/cnfm-modify-page";
|
||||
@@ -0,0 +1,318 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { dayjs } from "@/shared/lib";
|
||||
import { InstlSncnApi, type InstlSncnMngSearchParams } from "@/entities/instl-sncn";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
const TASK_OPTIONS = [
|
||||
{ value: "", label: "전체" },
|
||||
{ value: "SF", label: "하수처리시설" },
|
||||
{ value: "SP", label: "하수관로" },
|
||||
{ value: "RE", label: "재이용수" },
|
||||
];
|
||||
|
||||
const SIDO_OPTIONS = [
|
||||
{ value: "", label: "전체" },
|
||||
{ value: "11", label: "서울특별시" },
|
||||
{ value: "26", label: "부산광역시" },
|
||||
{ value: "27", label: "대구광역시" },
|
||||
{ value: "28", label: "인천광역시" },
|
||||
{ value: "29", label: "광주광역시" },
|
||||
{ value: "30", label: "대전광역시" },
|
||||
{ value: "31", label: "울산광역시" },
|
||||
{ value: "36", label: "세종특별자치시" },
|
||||
{ value: "41", label: "경기도" },
|
||||
{ value: "43", label: "충청북도" },
|
||||
{ value: "44", label: "충청남도" },
|
||||
{ value: "45", label: "전라북도" },
|
||||
{ value: "46", label: "전라남도" },
|
||||
{ value: "47", label: "경상북도" },
|
||||
{ value: "48", label: "경상남도" },
|
||||
{ value: "50", label: "제주특별자치도" },
|
||||
];
|
||||
|
||||
type StepCode = "SV" | "ST" | "ER" | "TE" | "TR" | "TP" | "TA";
|
||||
|
||||
const STEPS: { code: StepCode; label: string }[] = [
|
||||
{ code: "SV", label: "저장" },
|
||||
{ code: "ST", label: "신청완료" },
|
||||
{ code: "ER", label: "환경청 검토" },
|
||||
{ code: "TE", label: "기술검토" },
|
||||
{ code: "TR", label: "검토결과 회신" },
|
||||
{ code: "TP", label: "기술평가" },
|
||||
{ code: "TA", label: "최종승인" },
|
||||
];
|
||||
|
||||
const PAGE_GROUP_SIZE = 5;
|
||||
|
||||
function CnfmListPage() {
|
||||
const router = useRouter();
|
||||
const [bizNm, setBizNm] = useState("");
|
||||
const [taskSeComCd, setTaskSeComCd] = useState("");
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [search, setSearch] = useState<InstlSncnMngSearchParams>({
|
||||
bizNm: "",
|
||||
taskSeComCd: "",
|
||||
sidoComCd: "",
|
||||
sggComCd: "",
|
||||
});
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-list", search, pageIndex, pageSize],
|
||||
queryFn: () =>
|
||||
InstlSncnApi.getPage({
|
||||
...search,
|
||||
page: pageIndex,
|
||||
size: pageSize,
|
||||
sort: "regDt,desc",
|
||||
}),
|
||||
});
|
||||
|
||||
const totalPages = query.data?.totalPages ?? 0;
|
||||
|
||||
const pageGroupStart = Math.floor(pageIndex / PAGE_GROUP_SIZE) * PAGE_GROUP_SIZE;
|
||||
const pageNumbers = useMemo(() => {
|
||||
const end = Math.min(pageGroupStart + PAGE_GROUP_SIZE, totalPages);
|
||||
return Array.from({ length: end - pageGroupStart }, (_, i) => pageGroupStart + i);
|
||||
}, [pageGroupStart, totalPages]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPageIndex(0);
|
||||
setSearch({ bizNm, taskSeComCd, sidoComCd, sggComCd });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cnfm-page-wrap">
|
||||
{/* 검색 영역 */}
|
||||
<div className="cnfm-card">
|
||||
<form onSubmit={handleSearch} className="cnfm-search-bar">
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="sidoComCd">시도</label>
|
||||
<select
|
||||
id="sidoComCd"
|
||||
className="cnfm-select"
|
||||
value={sidoComCd}
|
||||
onChange={(e) => setSidoComCd(e.target.value)}
|
||||
>
|
||||
{SIDO_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="sggComCd">시군구</label>
|
||||
<select
|
||||
id="sggComCd"
|
||||
className="cnfm-select"
|
||||
value={sggComCd}
|
||||
onChange={(e) => setSggComCd(e.target.value)}
|
||||
>
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field cnfm-search-field--grow">
|
||||
<label className="cnfm-search-label" htmlFor="bizNm">사업명</label>
|
||||
<input
|
||||
id="bizNm"
|
||||
type="text"
|
||||
className="cnfm-input"
|
||||
value={bizNm}
|
||||
onChange={(e) => setBizNm(e.target.value)}
|
||||
placeholder="사업명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-search-field">
|
||||
<label className="cnfm-search-label" htmlFor="taskSeComCd">구분</label>
|
||||
<select
|
||||
id="taskSeComCd"
|
||||
className="cnfm-select cnfm-select--wide"
|
||||
value={taskSeComCd}
|
||||
onChange={(e) => setTaskSeComCd(e.target.value)}
|
||||
>
|
||||
{TASK_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="cnfm-btn-search">검색</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 목록 영역 */}
|
||||
<div className="cnfm-card mt-4">
|
||||
<div className="cnfm-list-header">
|
||||
<h3 className="cnfm-list-title">설치인가 목록</h3>
|
||||
<div className="cnfm-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-btn-register"
|
||||
onClick={() => router.push("/instl-sncn/register")}
|
||||
>
|
||||
✎ 신청
|
||||
</button>
|
||||
<select
|
||||
className="cnfm-select"
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 50].map((size) => (
|
||||
<option key={size} value={size}>{size}개</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-table-wrap">
|
||||
<table className="cnfm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>시도</th>
|
||||
<th>시군구</th>
|
||||
<th>사업명</th>
|
||||
<th>구분</th>
|
||||
<th>담당자</th>
|
||||
<th>해당기관</th>
|
||||
<th>진행상황</th>
|
||||
<th>시행일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{query.isLoading && (
|
||||
<tr>
|
||||
<td colSpan={8} className="cnfm-td-empty">조회 중입니다...</td>
|
||||
</tr>
|
||||
)}
|
||||
{!query.isLoading && (query.data?.content?.length ?? 0) === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="cnfm-td-empty">조회 결과가 없습니다.</td>
|
||||
</tr>
|
||||
)}
|
||||
{(query.data?.content ?? []).map((row) => {
|
||||
const currentStepIndex = STEPS.findIndex((s) => s.code === row.prgrsSttsComCd);
|
||||
return (
|
||||
<tr
|
||||
key={row.instlSncnDocNo}
|
||||
className="cnfm-row-clickable"
|
||||
onClick={() => router.push(`/instl-sncn/${row.instlSncnDocNo}`)}
|
||||
>
|
||||
<td>{row.sidoNm || "-"}</td>
|
||||
<td>{row.sggNm || "-"}</td>
|
||||
<td className="text-left">{row.bizNm || "-"}</td>
|
||||
<td>{row.taskSeCdNm || row.taskSeComCd || "-"}</td>
|
||||
<td>{row.chrgrNm || row.aplcntNm || "-"}</td>
|
||||
<td>{row.instNm || row.aplcntOgdpNm || "-"}</td>
|
||||
<td>
|
||||
<div className="cnfm-step-wrap">
|
||||
<div className="cnfm-step-row">
|
||||
{STEPS.slice(0, 4).map((step, index) => {
|
||||
const state =
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "current"
|
||||
: "pending";
|
||||
return (
|
||||
<span key={step.code} className={`cnfm-step-badge cnfm-step-badge--${state}`}>
|
||||
<span className={`cnfm-step-dot cnfm-step-dot--${state}`} />
|
||||
{step.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="cnfm-step-row">
|
||||
{STEPS.slice(4).map((step, i) => {
|
||||
const index = i + 4;
|
||||
const state =
|
||||
index < currentStepIndex
|
||||
? "completed"
|
||||
: index === currentStepIndex
|
||||
? "current"
|
||||
: "pending";
|
||||
return (
|
||||
<span key={step.code} className={`cnfm-step-badge cnfm-step-badge--${state}`}>
|
||||
<span className={`cnfm-step-dot cnfm-step-dot--${state}`} />
|
||||
{step.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{row.regDt ? dayjs(row.regDt).format("YYYY-MM-DD") : "-"}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="cnfm-pagination">
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-page-btn"
|
||||
onClick={() => setPageIndex(0)}
|
||||
disabled={pageIndex === 0}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-page-btn"
|
||||
onClick={() => setPageIndex((prev) => Math.max(0, prev - 1))}
|
||||
disabled={pageIndex === 0}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
{pageNumbers.map((pageNo) => (
|
||||
<button
|
||||
type="button"
|
||||
key={pageNo}
|
||||
className={`cnfm-page-btn ${pageNo === pageIndex ? "active" : ""}`}
|
||||
onClick={() => setPageIndex(pageNo)}
|
||||
>
|
||||
{pageNo + 1}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-page-btn"
|
||||
onClick={() => setPageIndex((prev) => Math.min(Math.max(0, totalPages - 1), prev + 1))}
|
||||
disabled={pageIndex >= totalPages - 1}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cnfm-page-btn"
|
||||
onClick={() => setPageIndex(Math.max(0, totalPages - 1))}
|
||||
disabled={pageIndex >= totalPages - 1}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnfmListPage };
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
import { InstlSncnApi } from "@/entities/instl-sncn";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
function CnfmModifyPage({ id }: { id: string }) {
|
||||
const router = useRouter();
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-modify", id],
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
});
|
||||
|
||||
if (query.isLoading) {
|
||||
return <div className="mt-6 text-[1.4rem]">수정 정보를 불러오는 중입니다...</div>;
|
||||
}
|
||||
|
||||
if (!query.data) {
|
||||
return <div className="mt-6 text-[1.4rem]">수정 대상을 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const data = query.data;
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="cnfm-card p-4">
|
||||
<h2 className="cnfm-section-title">설치인가 수정</h2>
|
||||
<p className="mt-2 text-[1.3rem] text-gray-600">
|
||||
상세 조회 API 데이터를 기반으로 수정 화면을 구성했습니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">기본 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>문서번호</th>
|
||||
<td>{data.instlSncnDocNo}</td>
|
||||
<th>진행상태</th>
|
||||
<td>{data.prgrsSttsCdNm || data.prgrsSttsComCd || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업명</th>
|
||||
<td>
|
||||
<Input defaultValue={data.bizNm || ""} className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>사업내용</th>
|
||||
<td>
|
||||
<Input defaultValue={data.bizCn || ""} className="!text-[1.3rem]" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자명</th>
|
||||
<td>
|
||||
<Input defaultValue={data.dsgnrNm || ""} className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>설계자 연락처</th>
|
||||
<td>
|
||||
<Input defaultValue={data.dsgnrTelno || ""} className="!text-[1.3rem]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-action-row">
|
||||
<Button color="white" onClick={() => router.push(`/instl-sncn/${id}`)}>상세로</Button>
|
||||
<Button type="button" onClick={() => window.alert("수정 API 연동은 추후 확장 가능합니다.")}>저장</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnfmModifyPage };
|
||||
@@ -0,0 +1,490 @@
|
||||
/* ============================
|
||||
페이지 레이아웃
|
||||
============================= */
|
||||
.cnfm-page-wrap {
|
||||
padding: 2rem 2.4rem;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
카드
|
||||
============================= */
|
||||
.cnfm-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 1.6rem 2rem;
|
||||
}
|
||||
|
||||
.cnfm-card.mt-4 {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
검색 바
|
||||
============================= */
|
||||
.cnfm-search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cnfm-search-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.cnfm-search-field--grow {
|
||||
flex: 1;
|
||||
min-width: 18rem;
|
||||
}
|
||||
|
||||
.cnfm-search-label {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cnfm-input {
|
||||
height: 3.6rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 0 1rem;
|
||||
font-size: 1.3rem;
|
||||
color: #111827;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cnfm-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.cnfm-select {
|
||||
height: 3.6rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 0 0.8rem;
|
||||
font-size: 1.3rem;
|
||||
color: #111827;
|
||||
background: #fff;
|
||||
min-width: 11rem;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cnfm-select--wide {
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.cnfm-select:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.cnfm-btn-search {
|
||||
height: 3.6rem;
|
||||
padding: 0 2rem;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cnfm-btn-search:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
목록 헤더
|
||||
============================= */
|
||||
.cnfm-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.cnfm-list-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.cnfm-list-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.cnfm-btn-register {
|
||||
height: 3.4rem;
|
||||
padding: 0 1.6rem;
|
||||
background: #22c55e;
|
||||
color: #fff;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cnfm-btn-register:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
테이블
|
||||
============================= */
|
||||
.cnfm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cnfm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-table th,
|
||||
.cnfm-table td {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 1.2rem 0.8rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cnfm-table thead th {
|
||||
background: #f8fafc;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.cnfm-table tbody td {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.cnfm-td-empty {
|
||||
padding: 3rem 0;
|
||||
color: #9ca3af;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-row-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cnfm-row-clickable:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
진행상황 스텝 배지
|
||||
============================= */
|
||||
.cnfm-step-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.cnfm-step-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.cnfm-step-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
border-radius: 9999px;
|
||||
padding: 0.3rem 0.9rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.cnfm-step-badge--completed {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.cnfm-step-badge--current {
|
||||
background: #fed7aa;
|
||||
border-color: #fb923c;
|
||||
color: #c2410c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-step-badge--pending {
|
||||
background: #f3f4f6;
|
||||
border-color: #d1d5db;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.cnfm-step-dot {
|
||||
display: inline-block;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cnfm-step-dot--completed {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.cnfm-step-dot--current {
|
||||
background: #f97316;
|
||||
}
|
||||
|
||||
.cnfm-step-dot--pending {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
페이지네이션
|
||||
============================= */
|
||||
.cnfm-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-top: 1.6rem;
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.cnfm-page-btn {
|
||||
min-width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
font-size: 1.3rem;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
padding: 0 0.6rem;
|
||||
}
|
||||
|
||||
.cnfm-page-btn:hover:not(:disabled) {
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.cnfm-page-btn.active {
|
||||
border-color: #3b82f6;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-page-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
폼 테이블 (상세/등록 페이지용)
|
||||
============================= */
|
||||
.cnfm-form-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-form-table th,
|
||||
.cnfm-form-table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.cnfm-form-table th {
|
||||
width: 16rem;
|
||||
background: #f9fafb;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-form-table td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cnfm-action-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
기존 호환 클래스 유지
|
||||
============================= */
|
||||
.cnfm-section-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.cnfm-search-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.cnfm-search-field {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.cnfm-search-field--grow {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.cnfm-section-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.cnfm-search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 8rem 1fr 6rem 16rem auto auto;
|
||||
gap: 1.2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cnfm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cnfm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-table th,
|
||||
.cnfm-table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 1rem 0.8rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cnfm-table thead th {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-row-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cnfm-row-clickable:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.cnfm-step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cnfm-step-badge {
|
||||
border-radius: 9999px;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #f9fafb;
|
||||
color: #6b7280;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
}
|
||||
|
||||
.cnfm-step-badge.completed {
|
||||
border-color: #93c5fd;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.cnfm-step-badge.current {
|
||||
border-color: #16a34a;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.cnfm-pagination button {
|
||||
min-width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-pagination button.active {
|
||||
border-color: #2563eb;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-form-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.cnfm-form-table th,
|
||||
.cnfm-form-table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.cnfm-form-table th {
|
||||
width: 16rem;
|
||||
background: #f9fafb;
|
||||
text-align: left;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cnfm-form-table td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cnfm-action-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.cnfm-search-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
function CnfmRegistPage() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="cnfm-card p-4">
|
||||
<h2 className="cnfm-section-title">설치인가 신청</h2>
|
||||
<p className="mt-2 text-[1.3rem] text-gray-600">
|
||||
AS-IS 화면 설계(신청 화면)를 반영한 입력 레이아웃입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">기본 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>사업명</th>
|
||||
<td>
|
||||
<Input placeholder="사업명 입력" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>사업구분</th>
|
||||
<td>
|
||||
<select className="h-[3.6rem] w-full rounded-md border border-gray-300 px-3 text-[1.3rem]">
|
||||
<option value="CNFM">설치인가</option>
|
||||
<option value="APR">설치승인</option>
|
||||
<option value="AFA">폐지인가</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업기간(시작)</th>
|
||||
<td>
|
||||
<Input placeholder="YYYY" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>사업기간(종료)</th>
|
||||
<td>
|
||||
<Input placeholder="YYYY" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">신청자 / 설계자 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>신청자명</th>
|
||||
<td>
|
||||
<Input placeholder="신청자명" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>신청자 연락처</th>
|
||||
<td>
|
||||
<Input placeholder="010-0000-0000" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자명</th>
|
||||
<td>
|
||||
<Input placeholder="설계자명" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
<th>설계자 소속</th>
|
||||
<td>
|
||||
<Input placeholder="설계자 소속" className="!text-[1.3rem]" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-action-row">
|
||||
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
|
||||
<Button type="button" onClick={() => window.alert("저장 API 연동은 추후 확장 가능합니다.")}>저장</Button>
|
||||
<Button type="button" onClick={() => window.alert("제출 API 연동은 추후 확장 가능합니다.")}>제출</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnfmRegistPage };
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { InstlSncnApi } from "@/entities/instl-sncn";
|
||||
import { dayjs } from "@/shared/lib";
|
||||
|
||||
import "./cnfm-page.css";
|
||||
|
||||
function CnfmViewPage({ id }: { id: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["instl-sncn-detail", id],
|
||||
queryFn: () => InstlSncnApi.getById(id),
|
||||
});
|
||||
|
||||
if (query.isLoading) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 불러오는 중입니다...</div>;
|
||||
}
|
||||
|
||||
if (!query.data) {
|
||||
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
const data = query.data;
|
||||
|
||||
return (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="cnfm-card p-4">
|
||||
<h2 className="cnfm-section-title">설치인가 상세보기</h2>
|
||||
<p className="mt-2 text-[1.3rem] text-gray-600">
|
||||
문서번호: <strong>{data.instlSncnDocNo}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">기본 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>사업명</th>
|
||||
<td>{data.bizNm || "-"}</td>
|
||||
<th>업무구분</th>
|
||||
<td>{data.taskSeCdNm || data.taskSeComCd || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업기간</th>
|
||||
<td>
|
||||
{(data.bizBgngYr || "-") + " ~ " + (data.bizEndYr || "-")}
|
||||
</td>
|
||||
<th>진행상태</th>
|
||||
<td>{data.prgrsSttsCdNm || data.prgrsSttsComCd || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>행정동ID</th>
|
||||
<td>{data.dongId || "-"}</td>
|
||||
<th>예산사업코드</th>
|
||||
<td>{data.bgtBizCd || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>사업내용</th>
|
||||
<td colSpan={3}>{data.bizCn || "-"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">담당자 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>신청자명</th>
|
||||
<td>{data.aplcntNm || "-"}</td>
|
||||
<th>신청자 연락처</th>
|
||||
<td>{data.aplcntMblTelno || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>신청자 소속</th>
|
||||
<td>{data.aplcntOgdpNm || "-"}</td>
|
||||
<th>신청자 이메일</th>
|
||||
<td>{data.aplcntEmlAddr || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자명</th>
|
||||
<td>{data.dsgnrNm || "-"}</td>
|
||||
<th>설계자 연락처</th>
|
||||
<td>{data.dsgnrTelno || "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>설계자 소속</th>
|
||||
<td>{data.dsgnrOgdpNm || "-"}</td>
|
||||
<th>설계자 이메일</th>
|
||||
<td>{data.dsgnrEmlAddr || "-"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-card p-4">
|
||||
<h3 className="text-[1.5rem] font-semibold">이력 정보</h3>
|
||||
<table className="cnfm-form-table mt-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>등록일시</th>
|
||||
<td>{data.regDt ? dayjs(data.regDt).format("YYYY-MM-DD HH:mm:ss") : "-"}</td>
|
||||
<th>수정일시</th>
|
||||
<td>{data.mdfcnDt ? dayjs(data.mdfcnDt).format("YYYY-MM-DD HH:mm:ss") : "-"}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>등록자명</th>
|
||||
<td>{data.rgtrNm || "-"}</td>
|
||||
<th>첨부파일그룹ID</th>
|
||||
<td>{data.atchFileGroupId || "-"}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="cnfm-action-row">
|
||||
<Button color="white" onClick={() => router.push("/instl-sncn")}>목록</Button>
|
||||
<Button onClick={() => router.push(`/instl-sncn/${id}/modify`)}>수정</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CnfmViewPage };
|
||||
@@ -1,5 +0,0 @@
|
||||
/** TBD: elaprTmpltId / aplnId 서버 발급 정책 확정 후 클라이언트 생성 규칙 교체 */
|
||||
export function generateClientId(prefix: string): string {
|
||||
const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 8).toUpperCase();
|
||||
return `${prefix}-${suffix}`;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { dayjs } from "./dayjs";
|
||||
export { generateClientId } from "./generate-client-id";
|
||||
export { matchQueryStatus } from "./match-query-status";
|
||||
export { queryClient } from "./query-client";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
|
||||
@@ -4,13 +4,12 @@ import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
import type { ElaprApln } from "@/entities/elapr";
|
||||
|
||||
import type { ElaprAplnRow } from "../model/types";
|
||||
|
||||
function toRow(item: ElaprApln): ElaprAplnRow {
|
||||
return { ...item, isNew: false, isDirty: false };
|
||||
return { ...item, rowKey: item.aplnId, isNew: false, isDirty: false };
|
||||
}
|
||||
|
||||
interface UseAplnRowsOptions {
|
||||
@@ -18,32 +17,36 @@ interface UseAplnRowsOptions {
|
||||
serverRows: ElaprApln[] | undefined;
|
||||
}
|
||||
|
||||
export function createDraftRowKey(): string {
|
||||
return `draft-${Date.now()}`;
|
||||
}
|
||||
|
||||
export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
const [rows, setRows] = useState<ElaprAplnRow[]>([]);
|
||||
const [editingRowId, setEditingRowId] = useState<string | null>(null);
|
||||
const [editingRowKey, setEditingRowKey] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elaprTmpltId) {
|
||||
setRows([]);
|
||||
setEditingRowId(null);
|
||||
setEditingRowKey(null);
|
||||
return;
|
||||
}
|
||||
setRows((serverRows ?? []).map(toRow));
|
||||
setEditingRowId(null);
|
||||
setEditingRowKey(null);
|
||||
}, [elaprTmpltId, serverRows]);
|
||||
|
||||
const isEditingLocked = editingRowId != null;
|
||||
const isEditingLocked = editingRowKey != null;
|
||||
|
||||
const addRow = useCallback(() => {
|
||||
if (!elaprTmpltId || isEditingLocked) return;
|
||||
|
||||
const tempId = generateClientId("APLN");
|
||||
const rowKey = createDraftRowKey();
|
||||
const nextSeq = rows.length + 1;
|
||||
|
||||
setRows((prev) => [
|
||||
...prev,
|
||||
{
|
||||
aplnId: tempId,
|
||||
rowKey,
|
||||
elaprTmpltId,
|
||||
atrzSeqNo: nextSeq,
|
||||
atrzSeComCd: "",
|
||||
@@ -53,40 +56,40 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
isDirty: true,
|
||||
},
|
||||
]);
|
||||
setEditingRowId(tempId);
|
||||
setEditingRowKey(rowKey);
|
||||
}, [elaprTmpltId, isEditingLocked, rows.length]);
|
||||
|
||||
const startEdit = useCallback(
|
||||
(aplnId: string) => {
|
||||
(rowKey: string) => {
|
||||
if (isEditingLocked) return;
|
||||
setEditingRowId(aplnId);
|
||||
setEditingRowKey(rowKey);
|
||||
setRows((prev) =>
|
||||
prev.map((row) => (row.aplnId === aplnId ? { ...row, snapshot: { ...row } } : row)),
|
||||
prev.map((row) => (row.rowKey === rowKey ? { ...row, snapshot: { ...row } } : row)),
|
||||
);
|
||||
},
|
||||
[isEditingLocked],
|
||||
);
|
||||
|
||||
const updateRow = useCallback(
|
||||
(aplnId: string, patch: Partial<ElaprAplnRow>) => {
|
||||
if (editingRowId !== aplnId) return;
|
||||
(rowKey: string, patch: Partial<ElaprAplnRow>) => {
|
||||
if (editingRowKey !== rowKey) return;
|
||||
setRows((prev) =>
|
||||
prev.map((row) => (row.aplnId === aplnId ? { ...row, ...patch, isDirty: true } : row)),
|
||||
prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch, isDirty: true } : row)),
|
||||
);
|
||||
},
|
||||
[editingRowId],
|
||||
[editingRowKey],
|
||||
);
|
||||
|
||||
const cancelEdit = useCallback(() => {
|
||||
if (!editingRowId) return;
|
||||
if (!editingRowKey) return;
|
||||
|
||||
setRows((prev) => {
|
||||
const target = prev.find((r) => r.aplnId === editingRowId);
|
||||
const target = prev.find((r) => r.rowKey === editingRowKey);
|
||||
if (target?.isNew) {
|
||||
return prev.filter((r) => r.aplnId !== editingRowId);
|
||||
return prev.filter((r) => r.rowKey !== editingRowKey);
|
||||
}
|
||||
return prev.map((row) => {
|
||||
if (row.aplnId !== editingRowId) return row;
|
||||
if (row.rowKey !== editingRowKey) return row;
|
||||
if (row.snapshot) {
|
||||
const { snapshot: _s, isNew: _n, isDirty: _d, ...restored } = row.snapshot;
|
||||
return { ...restored, isNew: false, isDirty: false };
|
||||
@@ -94,27 +97,27 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
return { ...row, isDirty: false };
|
||||
});
|
||||
});
|
||||
setEditingRowId(null);
|
||||
}, [editingRowId]);
|
||||
setEditingRowKey(null);
|
||||
}, [editingRowKey]);
|
||||
|
||||
const clearEditing = useCallback(() => {
|
||||
setEditingRowId(null);
|
||||
setEditingRowKey(null);
|
||||
}, []);
|
||||
|
||||
const removeRowLocal = useCallback((aplnId: string) => {
|
||||
setRows((prev) => prev.filter((r) => r.aplnId !== aplnId));
|
||||
setEditingRowId((id) => (id === aplnId ? null : id));
|
||||
const removeRowLocal = useCallback((rowKey: string) => {
|
||||
setRows((prev) => prev.filter((r) => r.rowKey !== rowKey));
|
||||
setEditingRowKey((key) => (key === rowKey ? null : key));
|
||||
}, []);
|
||||
|
||||
const reorderRows = useCallback(
|
||||
(activeId: string, overId: string): ElaprAplnRow[] => {
|
||||
(activeKey: string, overKey: string): ElaprAplnRow[] => {
|
||||
if (isEditingLocked) return [];
|
||||
|
||||
let newRows: ElaprAplnRow[] = [];
|
||||
|
||||
setRows((prev) => {
|
||||
const oldIndex = prev.findIndex((r) => r.aplnId === activeId);
|
||||
const newIndex = prev.findIndex((r) => r.aplnId === overId);
|
||||
const oldIndex = prev.findIndex((r) => r.rowKey === activeKey);
|
||||
const newIndex = prev.findIndex((r) => r.rowKey === overKey);
|
||||
if (oldIndex < 0 || newIndex < 0) return prev;
|
||||
|
||||
const moved = arrayMove(prev, oldIndex, newIndex).map((row, idx) => ({
|
||||
@@ -134,7 +137,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
return {
|
||||
rows,
|
||||
setRows,
|
||||
editingRowId,
|
||||
editingRowKey,
|
||||
isEditingLocked,
|
||||
addRow,
|
||||
startEdit,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { ElaprApln } from "@/entities/elapr";
|
||||
|
||||
/** 그리드 로컬 행 — API 응답 + UI 보조 필드 */
|
||||
export interface ElaprAplnRow extends ElaprApln {
|
||||
export interface ElaprAplnRow extends Omit<ElaprApln, "aplnId"> {
|
||||
/** React key · DnD · 편집 잠금용 — 서버 aplnId와 분리 */
|
||||
rowKey: string;
|
||||
aplnId?: string;
|
||||
/** 결재자 표시명 — 응답 DTO에 없음, 선택 시 로컬 유지 */
|
||||
aprvrNm?: string;
|
||||
isNew?: boolean;
|
||||
|
||||
@@ -36,7 +36,7 @@ interface ElaprAplnGridProps {
|
||||
|
||||
function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [pickingRowId, setPickingRowId] = useState<string | null>(null);
|
||||
const [pickingRowKey, setPickingRowKey] = useState<string | null>(null);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
@@ -53,7 +53,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
|
||||
const {
|
||||
rows,
|
||||
editingRowId,
|
||||
editingRowKey,
|
||||
isEditingLocked,
|
||||
addRow,
|
||||
startEdit,
|
||||
@@ -73,7 +73,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const payloadRows = reorderRows(String(active.id), String(over.id));
|
||||
reorderRows(String(active.id), String(over.id));
|
||||
};
|
||||
|
||||
const handleRowSave = async (row: ElaprAplnRow) => {
|
||||
@@ -82,7 +82,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
alert(fieldError);
|
||||
return;
|
||||
}
|
||||
const dupError = validateUniqueSeqNo(rows.map((r) => (r.aplnId === row.aplnId ? row : r)));
|
||||
const dupError = validateUniqueSeqNo(rows.map((r) => (r.rowKey === row.rowKey ? row : r)));
|
||||
if (dupError) {
|
||||
alert(dupError);
|
||||
return;
|
||||
@@ -92,13 +92,12 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
|
||||
if (row.isNew) {
|
||||
await createMutation.mutateAsync({
|
||||
aplnId: row.aplnId,
|
||||
elaprTmpltId,
|
||||
aprvrUuid: row.aprvrUuid,
|
||||
atrzSeComCd: row.atrzSeComCd,
|
||||
atrzSeqNo: row.atrzSeqNo,
|
||||
});
|
||||
} else {
|
||||
} else if (row.aplnId) {
|
||||
await updateMutation.mutateAsync({
|
||||
aplnId: row.aplnId,
|
||||
body: {
|
||||
@@ -118,11 +117,11 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
if (!window.confirm("선택한 결재선을 삭제하시겠습니까?")) return;
|
||||
|
||||
if (row.isNew) {
|
||||
removeRowLocal(row.aplnId);
|
||||
removeRowLocal(row.rowKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elaprTmpltId) return;
|
||||
if (!elaprTmpltId || !row.aplnId) return;
|
||||
|
||||
await deleteMutation.mutateAsync({ aplnId: row.aplnId, elaprTmpltId });
|
||||
await aplnQuery.refetch();
|
||||
@@ -148,13 +147,12 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
|
||||
if (row.isNew) {
|
||||
await createMutation.mutateAsync({
|
||||
aplnId: row.aplnId,
|
||||
elaprTmpltId,
|
||||
aprvrUuid: row.aprvrUuid,
|
||||
atrzSeComCd: row.atrzSeComCd,
|
||||
atrzSeqNo: row.atrzSeqNo,
|
||||
});
|
||||
} else {
|
||||
} else if (row.aplnId) {
|
||||
await updateMutation.mutateAsync({
|
||||
aplnId: row.aplnId,
|
||||
body: {
|
||||
@@ -172,12 +170,12 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
};
|
||||
|
||||
const handleReviewerSelect = (reviewer: ReviewerOption) => {
|
||||
if (!pickingRowId) return;
|
||||
updateRow(pickingRowId, {
|
||||
if (!pickingRowKey) return;
|
||||
updateRow(pickingRowKey, {
|
||||
aprvrUuid: reviewer.uuid,
|
||||
aprvrNm: reviewer.name,
|
||||
});
|
||||
setPickingRowId(null);
|
||||
setPickingRowKey(null);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -229,23 +227,23 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={rows.map((r) => r.aplnId)}
|
||||
items={rows.map((r) => r.rowKey)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{rows.map((row) => (
|
||||
<SortableAplnRow
|
||||
key={row.aplnId}
|
||||
key={row.rowKey}
|
||||
row={row}
|
||||
isEditing={editingRowId === row.aplnId}
|
||||
isEditing={editingRowKey === row.rowKey}
|
||||
isLocked={isEditingLocked}
|
||||
dragDisabled={isEditingLocked}
|
||||
onStartEdit={() => startEdit(row.aplnId)}
|
||||
onStartEdit={() => startEdit(row.rowKey)}
|
||||
onSave={() => handleRowSave(row)}
|
||||
onCancel={cancelEdit}
|
||||
onDelete={() => handleRowDelete(row)}
|
||||
onChange={(patch) => updateRow(row.aplnId, patch)}
|
||||
onChange={(patch) => updateRow(row.rowKey, patch)}
|
||||
onPickAprvr={() => {
|
||||
setPickingRowId(row.aplnId);
|
||||
setPickingRowKey(row.rowKey);
|
||||
setPickerOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ function SortableAplnRow({
|
||||
const { data: atrzSeCodes } = useCodeList("ATRZSECD");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: row.aplnId,
|
||||
id: row.rowKey,
|
||||
disabled: dragDisabled,
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ function SortableAplnRow({
|
||||
<Equal className="!size-6" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell align="center">{row.aplnId}</TableCell>
|
||||
<TableCell align="center">{row.isNew ? "-" : (row.aplnId ?? "-")}</TableCell>
|
||||
<TableCell align="center">
|
||||
{isEditing ? (
|
||||
<Input
|
||||
|
||||
@@ -86,12 +86,13 @@ function ElaprTmpltList({ selectedTmpltId, onSelectTmplt }: ElaprTmpltListProps)
|
||||
|
||||
const handleFormSubmit = async (values: ElaprTmpltFormValues) => {
|
||||
if (dialogMode === "create") {
|
||||
await createMutation.mutateAsync({
|
||||
elaprTmpltId: values.elaprTmpltId,
|
||||
const created = await createMutation.mutateAsync({
|
||||
elaprTmpltNm: values.elaprTmpltNm,
|
||||
docSeComCd: values.docSeComCd || undefined,
|
||||
elaprTmpltExpln: values.elaprTmpltExpln || undefined,
|
||||
});
|
||||
onSelectTmplt(created);
|
||||
setRowSelection({ [created.elaprTmpltId]: true });
|
||||
} else if (dialogMode === "edit" && editingTmplt) {
|
||||
await updateMutation.mutateAsync({
|
||||
elaprTmpltId: editingTmplt.elaprTmpltId,
|
||||
|
||||
Reference in New Issue
Block a user