Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e88cf0c6d0 | ||
|
|
882ed8d9eb |
@@ -1,9 +0,0 @@
|
||||
node_modules
|
||||
.next
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env*
|
||||
apps/**/node_modules
|
||||
packages/**/node_modules
|
||||
apps/**/.next
|
||||
**/.next/cache
|
||||
@@ -1,33 +0,0 @@
|
||||
name: Deploy Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- deploy
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -p 28 -H 192.168.0.4 >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key -p 28 onasys@192.168.0.4 << 'EOF'
|
||||
cd ~/frontend
|
||||
git restore .
|
||||
git clean -fd
|
||||
git pull origin deploy
|
||||
pnpm install
|
||||
pnpm turbo build
|
||||
pm2 reload all
|
||||
EOF
|
||||
+5
-18
@@ -1,10 +1,12 @@
|
||||
# turbo
|
||||
.turbo/
|
||||
.vscode/**/*
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
.idea/
|
||||
|
||||
# build
|
||||
.next/
|
||||
dist/
|
||||
|
||||
# build
|
||||
.next
|
||||
@@ -12,22 +14,7 @@ dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
apps/wr/.next/**/*
|
||||
apps/fo/.next/**/*
|
||||
|
||||
# env
|
||||
#.env*
|
||||
.env*
|
||||
.qodo
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
pnpm-lock.yaml
|
||||
settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
packages/**/tsconfig.tsbuildinfo
|
||||
logs/
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Branch: $BRANCH"
|
||||
echo "Changed packages (dry-run):"
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]" --dry-run
|
||||
echo "Starting build..."
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]"
|
||||
Vendored
-88
@@ -1,88 +0,0 @@
|
||||
{
|
||||
// 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
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pnpm": true
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
# ---------------------- #
|
||||
# STEP 1: Base 설치 환경 #
|
||||
# ---------------------- #
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
WORKDIR /repo
|
||||
|
||||
# pnpm 설치 (corepack)
|
||||
RUN corepack enable && corepack prepare pnpm@10.12.1 --activate
|
||||
|
||||
# 필요한 시스템 패키지 설치 (선택)
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# ---------------------- #
|
||||
# STEP 2: 의존성 설치용 캐시 레이어 #
|
||||
# ---------------------- #
|
||||
FROM base AS deps
|
||||
|
||||
# workspace 정의 복사
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY apps/cs/package.json ./apps/cs/
|
||||
COPY packages ./packages
|
||||
|
||||
# 종속성 설치 (only production dependencies)
|
||||
RUN rm -rf node_modules .next
|
||||
RUN pnpm install --frozen-lockfile --node-linker=node-modules
|
||||
#RUN pnpm install --frozen-lockfile --node-linker=node-modules
|
||||
RUN ls -al .
|
||||
#RUN pnpm install --prod
|
||||
|
||||
# ---------------------- #
|
||||
# STEP 3: 빌드 단계 #
|
||||
# ---------------------- #
|
||||
FROM base AS builder
|
||||
|
||||
COPY --from=deps /repo /repo
|
||||
WORKDIR /repo
|
||||
|
||||
# 전체 소스 복사
|
||||
COPY . .
|
||||
|
||||
RUN ls -al /repo/apps/cs/.next || true
|
||||
RUN rm -rf /repo/apps/cs/.next
|
||||
RUN ls -al /repo/apps/cs/
|
||||
# Next.js 빌드
|
||||
RUN cat /repo/apps/cs/next.config.ts || true
|
||||
RUN pnpm --filter ./apps/cs... build
|
||||
|
||||
#RUN ls -al .next/standalone
|
||||
# 👉 실행에 필요한 모듈만 유지
|
||||
#RUN pnpm prune --prod
|
||||
|
||||
# Next.js 빌드 내용 보기
|
||||
RUN ls -al ./apps/cs/
|
||||
RUN ls -al /repo/apps/cs/.next
|
||||
RUN ls -al /repo/apps/cs/.next/chunks || true
|
||||
|
||||
# web browser request zip format
|
||||
#RUN find /repo/apps/cs/.next/static -type f -name '*.js' -exec gzip -k {} \;
|
||||
|
||||
#RUN find ./apps/cs/.next -type f
|
||||
|
||||
# ---------------------- #
|
||||
# STEP 4: 최종 러너 단계 (최소 런타임) #
|
||||
# ---------------------- #
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# corepack 사용 설정
|
||||
#RUN corepack enable && corepack prepare pnpm@8.15.5 --activate
|
||||
|
||||
# 환경변수 설정
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
# pnpm은 런타임에 필요 없음 → corepack 설치 생략 가능
|
||||
|
||||
# 빌드 결과만 복사
|
||||
RUN rm -rf .next
|
||||
COPY --from=builder /repo/apps/cs/.next/standalone ./
|
||||
COPY --from=builder /repo/apps/cs/public ./public
|
||||
COPY --from=builder /repo/apps/cs/.next/static ./apps/cs/.next/static
|
||||
COPY --from=builder /repo/apps/cs/.env ./apps/cs/.env
|
||||
COPY --from=builder /repo/apps/cs/package.json ./apps/cs/package.json
|
||||
COPY --from=builder /repo/apps/cs/next.config.ts ./apps/cs/next.config.ts
|
||||
# 헬스체크 등 필요 시 추가 가능
|
||||
#HEALTHCHECK --interval=30s --timeout=3s CMD wget --spider -q http://localhost:3000 || exit 1
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "apps/cs/server.js"]
|
||||
#CMD ["pnpm", "start"]
|
||||
@@ -67,10 +67,10 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
|
||||
```json
|
||||
{
|
||||
// 기타 설정 ...
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@common/components": ["../../../packages/common/components/dist/"],
|
||||
"@common/config": ["../../../packages/common/config/dist/"],
|
||||
"@common/components/*": ["../../../packages/common/components/src/*"],
|
||||
"@common/components": ["../../../packages/common/components/src"],
|
||||
"@common/network/*": ["../../../packages/common/network/*"],
|
||||
"@common/network": ["../../../packages/common/network/"]
|
||||
}
|
||||
}
|
||||
@@ -95,47 +95,61 @@ export default {
|
||||
### 5. next.config.ts 수정
|
||||
```json
|
||||
import type { NextConfig } from "next";
|
||||
import webpack from "webpack";
|
||||
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
// common/components의 vite 환경 변수를 next 환경 변수로 정의
|
||||
new webpack.DefinePlugin({
|
||||
"import.meta.env.VITE_PORT": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PORT,
|
||||
),
|
||||
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST,
|
||||
),
|
||||
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL,
|
||||
),
|
||||
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
|
||||
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_CLAIM_NAME,
|
||||
),
|
||||
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_AUTH_USER_ID,
|
||||
),
|
||||
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
|
||||
}),
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
return [
|
||||
{
|
||||
source: "/server/:path*",
|
||||
destination: `${serverApiUrl}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
```
|
||||
|
||||
### 6. globals.css(또는 전역 css) 수정
|
||||
|
||||
```css
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
@import "@common/components/index.css";
|
||||
@import "@common/assets/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -151,26 +165,6 @@ rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 8. 로그아웃 처리 가이드
|
||||
|
||||
* 참고 파일
|
||||
- apps/cs/app/api/auth/logout/route.ts
|
||||
- packages/common/components/src/ui/header/index.tsx (55-65 line)
|
||||
|
||||
각 서비스 폴더에 ‘/app/api/auth/logout/route.ts’ 생성해야 합니다(코드는 cs에서 확인).
|
||||
|
||||
** 로그아웃 처리되면 ‘/auth/login’ 페이지로 이동됩니다.
|
||||
|
||||
### 9. 'SITE_ID’ 환경변수를 쿠키로 대체하는 가이드(공통 모듈 사용하기위한 필수 사항)
|
||||
|
||||
* 참고 파일: apps/cs/src/shared/api/client.ts (쿠키 저장 처리)
|
||||
apps/cs/src/shared/lib/cookie.ts (쿠키 공통 모듈)
|
||||
|
||||
`SITE_ID` 환경변수를 처음 사용하는 파일 또는 최상단 클라이언트 컴포넌트에서 “site-id-key” 쿠키에 `SITE_ID` 저장
|
||||
|
||||
|
||||
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- React(^18)
|
||||
@@ -196,9 +190,3 @@ http://dev.onasys.co.kr:8000
|
||||
|
||||
## Swagger
|
||||
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service
|
||||
|
||||
## 개발 가이드
|
||||
- 사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리
|
||||
- 쿠키명 : menu-code-key
|
||||
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
SERVER_API_URL=
|
||||
|
||||
# 401 시 브라우저 리다이렉트 대상
|
||||
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
# Template Application
|
||||
|
||||
Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **프레임워크**: Next.js 15 + TypeScript
|
||||
- **스타일링**: Tailwind CSS
|
||||
- **상태관리**: Zustand
|
||||
- **데이터 페칭**: TanStack Query (React Query)
|
||||
- **폼 관리**: React Hook Form + Zod
|
||||
- **테이블**: TanStack Table
|
||||
- **HTTP 클라이언트**: Axios
|
||||
- **날짜 처리**: Day.js
|
||||
|
||||
## 프로젝트 구조 (FSD + Next.js App Router)
|
||||
|
||||
이 프로젝트는 **Feature-Sliced Design v2.1**과 **FSD 공식 Next.js 가이드**에 맞춰 구성되어 있습니다. Next.js `app/`은 라우트만 담당하고, 실제 레이어는 `src/` 아래에 있습니다.
|
||||
|
||||
```
|
||||
_template/
|
||||
├── app/ # Next.js App Router (루트) — 라우트·레이아웃 진입점만
|
||||
│ ├── layout.tsx # 루트 레이아웃 (src/app에서 providers, styles import)
|
||||
│ └── (with-base-layout)/
|
||||
│ ├── layout.tsx # BaseLayout 래퍼 (src/app/layouts에서 import)
|
||||
│ └── page.tsx # re-export: export { MainPage as default } from '@/pages/home'
|
||||
│
|
||||
├── pages/ # 빈 폴더 + README (Next.js가 src/pages를 Pages Router로 쓰지 않도록)
|
||||
│ └── README.md
|
||||
│
|
||||
└── src/ # FSD 레이어 (실제 코드)
|
||||
├── app/ # App 레이어 (슬라이스 없음, 세그먼트만)
|
||||
│ ├── providers/ # 전역 Providers (QueryClient 등)
|
||||
│ ├── styles/ # 글로벌 스타일 (globals.css)
|
||||
│ └── layouts/ # 공통 레이아웃 컴포넌트 (BaseLayoutWrapper)
|
||||
│
|
||||
├── pages/ # Pages 레이어 (페이지 슬라이스)
|
||||
│ └── home/
|
||||
│ ├── ui/
|
||||
│ └── index.ts # Public API
|
||||
│
|
||||
├── widgets/ # Widgets 레이어
|
||||
├── features/ # Features 레이어
|
||||
├── entities/ # Entities 레이어
|
||||
└── shared/ # Shared 레이어
|
||||
├── ui/
|
||||
├── api/
|
||||
├── lib/
|
||||
└── config/
|
||||
```
|
||||
|
||||
### 계층 구조 이해하기
|
||||
|
||||
FSD는 **3단계 계층**으로 구성됩니다:
|
||||
|
||||
1. **Layers (레이어)**: 책임과 의존성에 따른 수평적 분리 (app, pages, widgets, features, entities, shared)
|
||||
2. **Slices (슬라이스)**: 비즈니스 도메인에 따른 수직적 분리 (user, product, auth-login 등)
|
||||
3. **Segments (세그먼트)**: 기술적 목적에 따른 분리 (ui, api, model, lib 등)
|
||||
|
||||
> 📚 각 레이어의 자세한 설명은 해당 폴더의 README.md를 참고하세요.
|
||||
|
||||
## FSD 레이어 설명
|
||||
|
||||
FSD는 계층적 아키텍처로, 각 레이어는 책임의 범위와 의존성의 정도에 따라 구분됩니다. 7개의 표준화된 레이어가 있으며, 프로젝트에 필요한 레이어만 사용하면 됩니다.
|
||||
|
||||
### 레이어 계층 구조 (위에서 아래로)
|
||||
|
||||
```
|
||||
app/ ↑ 가장 높은 책임, 가장 많은 의존성
|
||||
pages/ |
|
||||
widgets/ |
|
||||
features/ |
|
||||
entities/ |
|
||||
shared/ ↓ 가장 낮은 책임, 의존성 없음
|
||||
```
|
||||
|
||||
### 1. App
|
||||
- **목적**: 앱 전체에 영향을 주는 기술적/비즈니스적 사항
|
||||
- **내용**: Context providers, 글로벌 스타일, 스토어 설정, 라우터 설정, 애널리틱스
|
||||
- **특징**: 슬라이스가 없고 세그먼트로만 구성
|
||||
- **의존성**: 모든 하위 레이어 사용 가능
|
||||
|
||||
### 2. Pages (src/pages)
|
||||
- **목적**: 웹사이트나 애플리케이션을 구성하는 페이지/화면
|
||||
- **내용**: 페이지 UI, 로딩 상태, 에러 바운더리, 데이터 fetching
|
||||
- **특징**: 한 페이지당 한 슬라이스 (유사한 페이지는 그룹화 가능). Next.js `app/`의 route 파일에서 re-export
|
||||
- **의존성**: `widgets`, `features`, `entities`, `shared` 사용 가능
|
||||
|
||||
> 💡 **FSD v2.1 권장사항**: "pages-first" 접근 방식을 따라, 재사용되지 않는 큰 UI 블록과 비즈니스 로직은 페이지 슬라이스에 유지하세요.
|
||||
|
||||
### 3. Widgets
|
||||
- **목적**: 큰 규모의 독립적인 UI 블록
|
||||
- **내용**: 여러 features와 entities를 조합한 복합 UI
|
||||
- **사용 시기**: 여러 페이지에서 재사용되거나, 페이지에 여러 개의 큰 독립적 블록이 있을 때
|
||||
- **의존성**: `features`, `entities`, `shared` 사용 가능 (다른 `widgets` 불가)
|
||||
|
||||
### 4. Features
|
||||
- **목적**: 사용자가 수행하는 주요 상호작용과 비즈니스 기능
|
||||
- **내용**: 사용자 시나리오 (예: 로그인, 상품 검색, 댓글 작성)
|
||||
- **사용 시기**: 여러 페이지에서 재사용되는 기능
|
||||
- **의존성**: `entities`, `shared` 사용 가능 (다른 `features` 불가)
|
||||
|
||||
### 5. Entities
|
||||
- **목적**: 비즈니스가 다루는 실제 개념 (User, Product, Order 등)
|
||||
- **내용**: 데이터 모델, 타입, API 요청, 엔티티의 시각적 표현
|
||||
- **특징**: 비즈니스 용어로 명명
|
||||
- **의존성**: 다른 `entities`, `shared` 사용 가능
|
||||
|
||||
### 6. Shared
|
||||
- **목적**: 애플리케이션의 기반 제공
|
||||
- **내용**: API 클라이언트, UI Kit, 유틸리티 라이브러리, 글로벌 설정
|
||||
- **특징**: 비즈니스 로직 없음, 슬라이스가 없고 세그먼트로만 구성
|
||||
- **의존성**: 외부 라이브러리만 사용 가능 (다른 레이어 불가)
|
||||
|
||||
## 시작하기
|
||||
|
||||
```bash
|
||||
# 의존성 설치
|
||||
pnpm install
|
||||
|
||||
# 개발 서버 실행
|
||||
pnpm run dev
|
||||
|
||||
# 프로덕션 빌드
|
||||
pnpm run build
|
||||
|
||||
# 프로덕션 서버 실행
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 환경 변수
|
||||
|
||||
`.env.local` 파일 생성:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||
```
|
||||
|
||||
## FSD 핵심 규칙
|
||||
|
||||
### 1. Import 규칙
|
||||
|
||||
> **슬라이스 내의 모듈(파일)은 오직 하위 레이어에 있는 슬라이스만 import할 수 있습니다.**
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import (하위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { User } from "entities/user";
|
||||
import { Button } from "shared/ui/button";
|
||||
|
||||
// ❌ 잘못된 import (같은 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { PostForm } from "features/post-create"; // 금지!
|
||||
|
||||
// ❌ 잘못된 import (상위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { Header } from "widgets/header"; // 금지!
|
||||
```
|
||||
|
||||
### 2. Public API 규칙
|
||||
|
||||
> **슬라이스 외부에서는 Public API(index.ts)를 통해서만 접근해야 합니다.**
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import (Public API 사용)
|
||||
import { LoginForm } from "features/auth-login";
|
||||
|
||||
// ❌ 잘못된 import (내부 구조 직접 접근)
|
||||
import { LoginForm } from "features/auth-login/ui/login-form";
|
||||
```
|
||||
|
||||
**Public API 작성 시 주의사항**:
|
||||
- 와일드카드 re-export(`export * from`) 사용 지양
|
||||
- 필요한 것만 명시적으로 export
|
||||
- 슬라이스의 인터페이스를 명확하게 표현
|
||||
|
||||
```typescript
|
||||
// ❌ 나쁜 예시
|
||||
export * from "./ui/Comment";
|
||||
export * from "./model/comments";
|
||||
|
||||
// ✅ 좋은 예시
|
||||
export { Comment } from "./ui/comment";
|
||||
export { CommentForm } from "./ui/comment-form";
|
||||
export { useComments } from "./model/use-comments";
|
||||
export type { Comment, CommentFormData } from "./model/types";
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { BaseLayoutWrapper } from "@/app/layouts/base-layout";
|
||||
|
||||
interface WithBaseLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function WithBaseLayout({ children }: WithBaseLayoutProps) {
|
||||
return <BaseLayoutWrapper>{children}</BaseLayoutWrapper>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { MainPage as default } from "@/pages/home";
|
||||
@@ -1,169 +0,0 @@
|
||||
# App Layer
|
||||
|
||||
**App**은 애플리케이션 전체에 영향을 미치는 기술적, 비즈니스적 사항을 다루는 최상위 레이어입니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 애플리케이션의 진입점(entrypoint) 제공
|
||||
- 전역 Context Providers 설정
|
||||
- 라우터 구성
|
||||
- 글로벌 스타일 정의
|
||||
- 전역 상태 저장소 설정
|
||||
- 애널리틱스 및 모니터링 설정
|
||||
|
||||
## 특징
|
||||
|
||||
- **슬라이스 없음**: App 레이어는 Shared처럼 슬라이스가 없고 세그먼트로만 구성됩니다
|
||||
- **전체 앱 관심사**: 특정 비즈니스 도메인이 아닌 앱 전체와 관련된 코드만 포함
|
||||
- **최상위 레이어**: 모든 하위 레이어의 모듈을 자유롭게 import 가능
|
||||
|
||||
## 구조 (Next.js App Router)
|
||||
|
||||
Next.js를 사용하는 경우, App 레이어는 주로 `app/` 디렉토리로 표현됩니다:
|
||||
|
||||
```
|
||||
app/
|
||||
├── (routes)/ # 라우트 그룹 및 페이지
|
||||
│ ├── (auth)/
|
||||
│ │ ├── login/
|
||||
│ │ │ └── page.tsx
|
||||
│ │ └── register/
|
||||
│ │ └── page.tsx
|
||||
│ └── (main)/
|
||||
│ ├── dashboard/
|
||||
│ │ └── page.tsx
|
||||
│ └── settings/
|
||||
│ └── page.tsx
|
||||
├── layout.tsx # 루트 레이아웃
|
||||
├── page.tsx # 홈 페이지
|
||||
├── globals.css # 글로벌 스타일
|
||||
├── providers.tsx # 전역 Providers 설정
|
||||
└── error.tsx # 글로벌 에러 핸들러
|
||||
```
|
||||
|
||||
### 일반적인 FSD 세그먼트 구조
|
||||
|
||||
FSD 표준 세그먼트를 사용하는 경우:
|
||||
|
||||
```
|
||||
app/
|
||||
├── entrypoint/ # 애플리케이션 진입점 (프레임워크별)
|
||||
│ └── index.tsx
|
||||
├── providers/ # Context Providers
|
||||
│ ├── query-provider.tsx
|
||||
│ ├── theme-provider.tsx
|
||||
│ └── index.tsx
|
||||
├── styles/ # 글로벌 스타일
|
||||
│ ├── globals.css
|
||||
│ └── theme.css
|
||||
├── store/ # 글로벌 상태 저장소 설정
|
||||
│ └── index.ts
|
||||
└── routes/ # 라우터 설정
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
## 예시
|
||||
|
||||
### Next.js Providers 설정
|
||||
|
||||
```typescript
|
||||
// app/providers.tsx
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
import { Providers } from './providers';
|
||||
import './globals.css';
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 글로벌 설정
|
||||
|
||||
```typescript
|
||||
// app/config/analytics.ts
|
||||
export function initAnalytics() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 애널리틱스 초기화
|
||||
}
|
||||
}
|
||||
|
||||
// app/config/monitoring.ts
|
||||
export function initMonitoring() {
|
||||
// 에러 모니터링 설정
|
||||
}
|
||||
```
|
||||
|
||||
## Next.js App Router와 FSD
|
||||
|
||||
Next.js의 `app/` 디렉토리는 FSD의 **App 레이어**와 **Pages 레이어**를 동시에 담당합니다:
|
||||
|
||||
- **루트 레벨 파일들** (`layout.tsx`, `providers.tsx`, `globals.css`): App 레이어
|
||||
- **라우트 폴더들** (`dashboard/`, `login/` 등): Pages 레이어
|
||||
|
||||
각 페이지별 비즈니스 로직은:
|
||||
- 재사용되지 않는 UI는 페이지 슬라이스(`app/dashboard/` 등) 안에 유지
|
||||
- 재사용되는 기능은 `features/` 레이어로 분리
|
||||
- 여러 페이지에서 재사용되는 큰 UI 블록은 `widgets/` 레이어로 분리
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
- 모든 하위 레이어 (`pages`, `widgets`, `features`, `entities`, `shared`)
|
||||
- 같은 `app` 레이어 내의 다른 세그먼트
|
||||
- 외부 라이브러리
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
- 없음 (App은 최상위 레이어)
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **비대화 방지**: App 레이어가 너무 커지면 관리가 어려워집니다. 대부분의 로직은 하위 레이어에 배치하세요
|
||||
2. **전역 관심사만**: 특정 페이지나 기능에만 관련된 코드는 해당 레이어로 이동
|
||||
3. **라우터 레벨 로직**: 라우터 및 서버 레벨 로직은 이 레이어에 유지
|
||||
4. **Processes 레이어 사용 지양**: Processes 레이어는 deprecated되었으므로 해당 로직은 `app`이나 `features`에 배치
|
||||
|
||||
## FSD v2.1 권장사항
|
||||
|
||||
최신 FSD 버전(v2.1)은 **"pages-first"** 접근 방식을 권장합니다:
|
||||
|
||||
- 더 많은 코드를 페이지 슬라이스에 유지
|
||||
- 큰 UI 블록과 재사용되지 않는 비즈니스 로직은 즉시 추출하지 말고 페이지에 보관
|
||||
- 실제로 재사용이 필요할 때만 `features`나 `widgets`로 분리
|
||||
|
||||
이 접근 방식은 과도한 추상화를 방지하고 코드를 더 쉽게 탐색할 수 있게 합니다.
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "행정지원시스템",
|
||||
description: "행정지원시스템",
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,50 +0,0 @@
|
||||
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";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;");
|
||||
return response;
|
||||
}
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
const logMessages: string[] = [
|
||||
`[Middleware Request] ${new Date().toISOString()}`,
|
||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
|
||||
`[Request]\n Original Pathname: ${pathname}`,
|
||||
];
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
const response = NextResponse.rewrite(new URL(newUrl));
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
// if (pathname === "/") {
|
||||
// const response = NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
// return addSecurityHeaders(response);
|
||||
// }
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
const response = NextResponse.next();
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/"],
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@common/assets"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.8.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.5.18",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.11",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# 이 폴더는 비워 둡니다 (FSD + Next.js App Router)
|
||||
|
||||
Next.js가 `src/pages`를 **Pages Router**용 `pages` 디렉터리로 인식하지 않도록,
|
||||
프로젝트 루트에 **빈 `pages` 폴더**를 두는 것이 FSD 공식 가이드 권장사항입니다.
|
||||
|
||||
- 실제 페이지(FSD Pages 레이어)는 **`src/pages`**에 있습니다.
|
||||
- Next.js 라우트는 **`app/`** 에서 `src/pages`를 re-export 하는 방식으로 연결됩니다.
|
||||
|
||||
[Feature-Sliced Design - Usage with Next.js (App Router)](https://feature-sliced.design/docs/guides/tech/with-nextjs)
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
interface BaseLayoutWrapperProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(item: MenuItem) => {
|
||||
router.push(item.urlPath);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { queryClient } from "@/shared/lib";
|
||||
|
||||
interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Providers({ children }: ProvidersProps) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
@import "@common/components/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
# Entities Layer
|
||||
|
||||
**Entities**는 프로젝트가 다루는 실제 비즈니스 개념을 나타내는 레이어입니다. 비즈니스에서 제품을 설명할 때 사용하는 용어들(예: User, Product, Order)이 주로 엔티티가 됩니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 비즈니스 엔티티의 데이터 모델 및 타입 정의
|
||||
- 엔티티 관련 API 요청 함수 관리
|
||||
- 엔티티의 시각적 표현 제공 (재사용 가능한 UI)
|
||||
- 여러 페이지에서 동일한 외관을 재사용할 수 있도록 지원
|
||||
|
||||
## 구조
|
||||
|
||||
각 엔티티 슬라이스는 다음 세그먼트로 구성됩니다:
|
||||
|
||||
```
|
||||
entities/
|
||||
└── [entity-name]/
|
||||
├── model/ # 데이터 저장소, 검증 스키마, 상태 관리
|
||||
├── api/ # 엔티티 관련 API 요청 함수
|
||||
├── ui/ # 엔티티 시각적 표현 (선택적)
|
||||
├── lib/ # 엔티티 관련 유틸리티 (선택적)
|
||||
├── @x/ # 다른 엔티티로부터의 cross-import용 Public API (선택적)
|
||||
└── index.ts # Public API
|
||||
```
|
||||
|
||||
### 세그먼트 설명
|
||||
|
||||
- **model**: 데이터 모델, 타입 정의, 상태 저장소 (Zustand 등)
|
||||
- **api**: 백엔드 API 호출 함수
|
||||
- **ui**: 엔티티를 표시하는 UI 컴포넌트 (완전한 UI 블록이 아니라 재사용 가능한 표현)
|
||||
- **lib**: 엔티티 관련 유틸리티 함수
|
||||
- **@x**: 다른 엔티티가 이 엔티티를 import할 때 사용하는 명시적 cross-import API
|
||||
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
|
||||
|
||||
## 예시
|
||||
|
||||
```typescript
|
||||
// entities/user/model/user.ts
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'admin' | 'user';
|
||||
}
|
||||
|
||||
// entities/user/api/get-user.ts
|
||||
import type { User } from '../model/user';
|
||||
|
||||
export async function getUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// entities/user/ui/user-card.tsx
|
||||
import type { User } from '../model/user';
|
||||
|
||||
interface UserCardProps {
|
||||
user: User;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function UserCard({ user, onClick }: UserCardProps) {
|
||||
return (
|
||||
<div onClick={onClick}>
|
||||
<h3>{user.name}</h3>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// entities/user/index.ts
|
||||
export { type User } from './model/user';
|
||||
export { getUser } from './api/get-user';
|
||||
export { UserCard } from './ui/user-card';
|
||||
```
|
||||
|
||||
## 엔티티 간 관계
|
||||
|
||||
엔티티가 서로 연결되어 있을 때는 `@x` 표기법을 사용하여 명시적으로 cross-import를 관리하는 것이 권장됩니다:
|
||||
|
||||
```typescript
|
||||
// entities/artist/model/artist.ts
|
||||
import type { Song } from 'entities/song/@x/artist';
|
||||
|
||||
export interface Artist {
|
||||
name: string;
|
||||
songs: Array<Song>;
|
||||
}
|
||||
|
||||
// entities/song/@x/artist.ts
|
||||
export type { Song } from '../model/song';
|
||||
```
|
||||
|
||||
> `@x` 폴더를 사용하면 엔티티 간 연결을 명확하게 만들어 함께 리팩터링이 필요한 코드를 쉽게 찾을 수 있습니다.
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
- `shared` 레이어의 모듈
|
||||
- 같은 엔티티 슬라이스 내의 다른 세그먼트
|
||||
- 다른 엔티티 슬라이스 (필요한 경우, `@x` 표기법 권장)
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
- `features` 레이어
|
||||
- `widgets` 레이어
|
||||
- `pages` 레이어
|
||||
- `app` 레이어
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **UI 컴포넌트는 완전한 비즈니스 로직을 포함하지 않음**: UI는 주로 props나 slots를 통해 다양한 비즈니스 로직을 연결할 수 있도록 설계
|
||||
2. **엔티티 간 비즈니스 로직은 상위 레이어에**: 엔티티 간 상호작용의 비즈니스 로직은 Features나 Pages 레이어에서 처리
|
||||
3. **데이터 관리에 집중**: 복잡한 비즈니스 로직보다는 데이터 모델, 검증, 저장에 집중
|
||||
@@ -1 +0,0 @@
|
||||
export { type MenuItem } from "./model/types";
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
# Features Layer
|
||||
|
||||
**Features**는 앱의 주요 사용자 상호작용과 기능을 구현하는 레이어입니다. 사용자가 실제로 수행하는 작업들로, 주로 비즈니스 엔티티와 관련이 있습니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 사용자가 중요하게 생각하는 주요 상호작용 구현
|
||||
- 재사용 가능한 비즈니스 기능 제공
|
||||
- 여러 페이지에서 사용되는 기능의 중복 제거
|
||||
|
||||
## Feature로 만들어야 하는 경우
|
||||
|
||||
> ⚠️ **중요**: 모든 것을 feature로 만들 필요는 없습니다.
|
||||
|
||||
Feature로 만드는 것이 좋은 지표:
|
||||
|
||||
- **여러 페이지에서 재사용됨**: 예를 들어, 여러 에디터가 있고 모두 댓글 기능이 있다면, 댓글은 재사용 가능한 feature입니다
|
||||
- **중요한 비즈니스 기능**: 신규 프로젝트 참여자가 Features 폴더를 보고 앱의 주요 기능을 빠르게 파악할 수 있어야 합니다
|
||||
|
||||
Feature가 너무 많으면 중요한 기능이 묻히므로, 신규 참여자의 경험을 최적화하여 결정하세요.
|
||||
|
||||
## 구조
|
||||
|
||||
각 feature 슬라이스는 다음 세그먼트로 구성됩니다:
|
||||
|
||||
```
|
||||
features/
|
||||
└── [feature-name]/
|
||||
├── ui/ # 상호작용을 수행하는 UI (폼, 버튼 등)
|
||||
├── api/ # 액션 수행에 필요한 API 호출 (선택적)
|
||||
├── model/ # 검증 및 내부 상태 관리 (선택적)
|
||||
├── lib/ # 유틸리티 함수 (선택적)
|
||||
├── config/ # feature flags (선택적)
|
||||
└── index.ts # Public API
|
||||
```
|
||||
|
||||
### 세그먼트 설명
|
||||
|
||||
- **ui**: 사용자 상호작용을 위한 UI 컴포넌트 (폼, 모달, 버튼 등)
|
||||
- **api**: feature 실행에 필요한 API 요청 함수
|
||||
- **model**: 폼 검증, 내부 상태, 비즈니스 로직
|
||||
- **lib**: feature 내부에서만 사용되는 유틸리티
|
||||
- **config**: feature flags, feature별 설정값
|
||||
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
|
||||
|
||||
## 예시
|
||||
|
||||
```typescript
|
||||
// features/auth-login/ui/login-form.tsx
|
||||
import { useState } from 'react';
|
||||
import { loginUser } from '../api/login-user';
|
||||
import type { LoginCredentials } from '../model/types';
|
||||
|
||||
export function LoginForm() {
|
||||
const [credentials, setCredentials] = useState<LoginCredentials>({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
await loginUser(credentials);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({ ...credentials, email: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
|
||||
/>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// features/auth-login/api/login-user.ts
|
||||
import type { LoginCredentials } from '../model/types';
|
||||
|
||||
export async function loginUser(credentials: LoginCredentials) {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// features/auth-login/model/types.ts
|
||||
export interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// features/auth-login/index.ts
|
||||
export { LoginForm } from './ui/login-form';
|
||||
export { loginUser } from './api/login-user';
|
||||
export type { LoginCredentials } from './model/types';
|
||||
```
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
|
||||
- `shared` 레이어의 모듈
|
||||
- `entities` 레이어의 모듈
|
||||
- 같은 feature 슬라이스 내의 다른 세그먼트
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
|
||||
- 다른 `features` 슬라이스 (feature 간 직접 의존 금지)
|
||||
- `widgets` 레이어
|
||||
- `pages` 레이어
|
||||
- `app` 레이어
|
||||
|
||||
> 💡 **팁**: 여러 features가 공통 기능을 필요로 한다면, 그 기능을 `shared` 레이어로 이동시키거나 `entities` 레이어에서 관리하세요.
|
||||
|
||||
## 명명 규칙
|
||||
|
||||
- 소문자와 대시 사용: `auth-login`, `post-create`, `comment-edit`
|
||||
- 동사 형태 권장: 사용자 액션을 나타내는 이름 사용
|
||||
- 명확하고 구체적으로: 기능의 목적이 명확하게 드러나도록
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **하나의 기능에만 집중**: 각 feature는 단일 사용자 시나리오를 구현해야 합니다
|
||||
2. **재사용성 고려**: 여러 페이지에서 사용될 수 있도록 설계
|
||||
3. **독립성 유지**: 다른 features에 의존하지 않고 독립적으로 작동
|
||||
4. **적절한 추상화**: 너무 많은 feature를 만들지 말고, 중요한 기능 위주로 구성
|
||||
@@ -1 +0,0 @@
|
||||
export { MainPage } from "./ui/main-page";
|
||||
@@ -1,9 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export function MainPage() {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-10">
|
||||
<h1 className="mb-4 text-3xl font-bold">Home</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
# Shared Layer
|
||||
|
||||
**Shared**는 애플리케이션의 기반을 형성하는 레이어입니다. 외부 세계(백엔드, 서드파티 라이브러리, 환경)와의 연결을 생성하고, 높은 응집력을 가진 자체 라이브러리를 정의하는 곳입니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 외부 시스템 및 라이브러리와의 통합
|
||||
- 프로젝트 전체에서 사용되는 공통 코드 제공
|
||||
- 비즈니스 로직이 없는 재사용 가능한 유틸리티 및 UI 제공
|
||||
- 글로벌 설정 및 환경 변수 관리
|
||||
|
||||
## 특징
|
||||
|
||||
- **슬라이스 없음**: Shared 레이어는 비즈니스 도메인이 없으므로 슬라이스로 나뉘지 않습니다
|
||||
- **세그먼트 구성**: 기술적 목적에 따라 세그먼트로 직접 구성됩니다
|
||||
- **자유로운 import**: Shared 내의 모든 파일은 서로를 참조하고 import할 수 있습니다
|
||||
|
||||
## 구조
|
||||
|
||||
```
|
||||
shared/
|
||||
├── ui/ # 애플리케이션 UI Kit
|
||||
│ ├── button/
|
||||
│ ├── input/
|
||||
│ ├── modal/
|
||||
│ └── index.ts
|
||||
├── api/ # API 클라이언트 및 백엔드 엔드포인트 함수
|
||||
│ ├── client.ts
|
||||
│ └── index.ts
|
||||
├── lib/ # 내부 라이브러리 모음
|
||||
│ ├── dates/ # 날짜 관련 유틸리티
|
||||
│ ├── colors/ # 색상 관련 유틸리티
|
||||
│ ├── text/ # 텍스트 처리 유틸리티
|
||||
│ └── index.ts
|
||||
├── config/ # 환경 변수, 글로벌 feature flags
|
||||
│ ├── env.ts
|
||||
│ └── index.ts
|
||||
├── types/ # 공통 타입 정의
|
||||
│ └── index.ts
|
||||
├── i18n/ # 번역 설정, 글로벌 번역 문자열
|
||||
│ └── index.ts
|
||||
└── routes/ # 라우트 상수 또는 패턴
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## 세그먼트 설명
|
||||
|
||||
### 📁 ui
|
||||
애플리케이션의 UI Kit로, 비즈니스 로직을 포함하지 않는 재사용 가능한 UI 컴포넌트들입니다.
|
||||
|
||||
- Button, Input, Modal 같은 기본 컴포넌트
|
||||
- 비즈니스 로직은 없지만 UI 로직은 포함 가능 (예: Autocomplete, SearchBar)
|
||||
- 비즈니스 테마 컴포넌트도 허용 (예: 회사 로고, 페이지 레이아웃)
|
||||
|
||||
### 📁 api
|
||||
API 클라이언트 및 특정 백엔드 엔드포인트 요청 함수입니다.
|
||||
|
||||
- HTTP 클라이언트 설정 (Axios, Fetch 등)
|
||||
- 인증 인터셉터
|
||||
- 공통 에러 핸들링
|
||||
|
||||
### 📁 lib
|
||||
내부 라이브러리 모음으로, 각 라이브러리는 **하나의 관심사**에 집중해야 합니다.
|
||||
|
||||
> ⚠️ **중요**: `helpers`나 `utils` 폴더처럼 덤프 폴더로 만들지 마세요. 각 라이브러리는 명확한 초점 영역(예: dates, colors, text)을 가져야 하며, README로 문서화해야 합니다.
|
||||
|
||||
### 📁 config
|
||||
환경 변수, 글로벌 feature flags 및 앱 전체 설정입니다.
|
||||
|
||||
### 📁 types
|
||||
프로젝트 전체에서 사용되는 공통 타입 정의입니다.
|
||||
|
||||
### 📁 i18n
|
||||
번역 설정 코드 및 글로벌 번역 문자열입니다.
|
||||
|
||||
### 📁 routes
|
||||
라우트 상수 또는 라우트 매칭 패턴입니다.
|
||||
|
||||
## 예시
|
||||
|
||||
```typescript
|
||||
// shared/ui/button/button.tsx
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
variant?: 'primary' | 'secondary';
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={variant === 'primary' ? 'bg-blue-500' : 'bg-gray-500'}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// shared/api/client.ts
|
||||
import axios from 'axios';
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// shared/lib/dates/format-date.ts
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
// shared/config/env.ts
|
||||
export const ENV = {
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
||||
isDev: process.env.NODE_ENV === 'development',
|
||||
} as const;
|
||||
```
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
- 같은 `shared` 레이어 내의 다른 세그먼트 (자유로운 cross-import)
|
||||
- 외부 라이브러리 및 패키지
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
- `entities` 레이어
|
||||
- `features` 레이어
|
||||
- `widgets` 레이어
|
||||
- `pages` 레이어
|
||||
- `app` 레이어
|
||||
|
||||
> 💡 **원칙**: Shared는 가장 낮은 레이어이므로 상위 레이어에 의존하지 않습니다.
|
||||
|
||||
## 세그먼트 명명 규칙
|
||||
|
||||
세그먼트 이름은 **내용의 목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다.
|
||||
|
||||
❌ **나쁜 예시**: `components`, `hooks`, `types` (본질을 설명)
|
||||
✅ **좋은 예시**: `ui`, `api`, `lib/dates`, `config` (목적을 설명)
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **비즈니스 로직 없음**: Shared는 순수한 기술적 코드만 포함
|
||||
2. **높은 재사용성**: 앱 전체에서 사용될 수 있어야 함
|
||||
3. **명확한 라이브러리 구조**: `lib` 내부는 관심사별로 명확하게 분리
|
||||
4. **문서화**: 각 라이브러리는 README로 사용법과 목적을 문서화
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Client } from "@common/network";
|
||||
import { PROXY_HOST, SITE_ID } from "@/shared/config/env";
|
||||
|
||||
const BASE_URL = `${PROXY_HOST}/server`;
|
||||
|
||||
export const apiClient = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { apiClient } from "./client";
|
||||
@@ -1,8 +0,0 @@
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "AS";
|
||||
export const SERVER_API_URL = process.env.SERVER_API_URL;
|
||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
||||
export const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
|
||||
export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./env";
|
||||
@@ -1,12 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/ko";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.locale("ko");
|
||||
|
||||
export { dayjs };
|
||||
@@ -1,2 +0,0 @@
|
||||
export { queryClient } from "./query-client";
|
||||
export { dayjs } from "./dayjs";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
export interface BaseEntity {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface PaginationResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { PageLayout } from "./page-layout";
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { PageTitle } from "@common/components";
|
||||
|
||||
const mockCurrentMenuInfo = {
|
||||
menuCd: 0,
|
||||
menuKornNm: "메인",
|
||||
menuEngNm: "Main",
|
||||
parentId: null,
|
||||
sortSeq: 0,
|
||||
children: null,
|
||||
levelNo: 0,
|
||||
urlPath: "/",
|
||||
menuLnkgTypeCd: "",
|
||||
menuLnkgSn: 0,
|
||||
menuPath: "메인",
|
||||
qrcd: "",
|
||||
useYn: "Y",
|
||||
otptYn: "Y",
|
||||
newWndwYn: "N",
|
||||
requiredUrlPath: false,
|
||||
};
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const menuPaths = mockCurrentMenuInfo.menuPath.split(">").map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="bg-container h-auto min-h-full pb-2">
|
||||
<PageTitle title={mockCurrentMenuInfo.menuKornNm} menuPaths={menuPaths} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLayout };
|
||||
@@ -1,135 +0,0 @@
|
||||
# Widgets Layer
|
||||
|
||||
**Widgets**는 큰 규모의 독립적인 UI 블록을 위한 레이어입니다. 여러 features와 entities를 조합하여 완전한 기능을 제공하는 복합 컴포넌트를 담습니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 큰 규모의 독립적인 UI 블록 구성
|
||||
- 여러 features와 entities를 하나의 응집력 있는 UI로 결합
|
||||
- 여러 페이지에서 재사용되는 복잡한 UI 제공
|
||||
- 페이지 레벨 레이아웃 및 섹션 관리
|
||||
|
||||
## Widget으로 만들어야 하는 경우
|
||||
|
||||
Widget은 다음 조건에 해당할 때 사용하세요:
|
||||
|
||||
✅ **Widget으로 만들기 좋은 경우**:
|
||||
- 여러 페이지에서 재사용되는 큰 UI 블록
|
||||
- 페이지에 여러 개의 큰 독립적 블록이 있고, 이것이 그 중 하나인 경우
|
||||
- 페이지 레벨 레이아웃 컴포넌트 (nested routing 시스템 사용 시)
|
||||
|
||||
❌ **Widget으로 만들지 말아야 하는 경우**:
|
||||
- UI 블록이 페이지의 대부분을 차지하고 재사용되지 않는 경우 → 페이지 슬라이스 내부에 직접 배치
|
||||
- 작은 재사용 컴포넌트 → `shared/ui`에 배치
|
||||
|
||||
## 구조
|
||||
|
||||
각 widget 슬라이스는 다음 세그먼트로 구성됩니다:
|
||||
|
||||
```
|
||||
widgets/
|
||||
└── [widget-name]/
|
||||
├── ui/ # 위젯의 메인 UI 컴포넌트
|
||||
├── model/ # 위젯 내부 상태 관리 (선택적)
|
||||
├── api/ # 위젯 전용 API 호출 (선택적)
|
||||
├── lib/ # 위젯 내부 유틸리티 (선택적)
|
||||
└── index.ts # Public API
|
||||
```
|
||||
|
||||
### 세그먼트 설명
|
||||
|
||||
- **ui**: 여러 features와 entities를 조합한 위젯 UI
|
||||
- **model**: 위젯 내부 상태 및 로직 (선택적)
|
||||
- **api**: 위젯이 필요로 하는 데이터 fetching (선택적)
|
||||
- **lib**: 위젯 내부에서만 사용되는 헬퍼 함수 (선택적)
|
||||
- **index.ts**: 슬라이스의 Public API (외부에서 사용할 수 있는 것만 export)
|
||||
|
||||
## 예시
|
||||
|
||||
```typescript
|
||||
// widgets/header/ui/header.tsx
|
||||
import { Logo } from 'shared/ui/logo';
|
||||
import { Navigation } from 'shared/ui/navigation';
|
||||
import { UserMenu } from 'features/user-menu';
|
||||
import { NotificationBell } from 'features/notifications';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header>
|
||||
<Logo />
|
||||
<Navigation />
|
||||
<NotificationBell />
|
||||
<UserMenu />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// widgets/product-list/ui/product-list.tsx
|
||||
import { useProducts } from '../api/use-products';
|
||||
import { ProductCard } from 'entities/product';
|
||||
import { AddToCartButton } from 'features/cart-add-item';
|
||||
|
||||
export function ProductList() {
|
||||
const { data: products, isLoading } = useProducts();
|
||||
|
||||
if (isLoading) return <div>로딩 중...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{products.map((product) => (
|
||||
<div key={product.id}>
|
||||
<ProductCard product={product} />
|
||||
<AddToCartButton productId={product.id} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// widgets/product-list/api/use-products.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getProducts } from 'entities/product';
|
||||
|
||||
export function useProducts() {
|
||||
return useQuery({
|
||||
queryKey: ['products'],
|
||||
queryFn: getProducts,
|
||||
});
|
||||
}
|
||||
|
||||
// widgets/product-list/index.ts
|
||||
export { ProductList } from './ui/product-list';
|
||||
```
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
- `shared` 레이어의 모듈
|
||||
- `entities` 레이어의 모듈
|
||||
- `features` 레이어의 모듈
|
||||
- 같은 widget 슬라이스 내의 다른 세그먼트
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
- 다른 `widgets` 슬라이스 (widget 간 직접 의존 금지)
|
||||
- `pages` 레이어
|
||||
- `app` 레이어
|
||||
|
||||
## 명명 규칙
|
||||
|
||||
- 소문자와 대시 사용: `header`, `sidebar`, `product-list`
|
||||
- 명사 형태 권장: UI 블록의 성격을 나타내는 이름 사용
|
||||
- 구체적으로: 위젯의 역할이 명확하게 드러나도록
|
||||
|
||||
## Nested Routing 사용 시
|
||||
|
||||
Remix 같은 nested routing 시스템을 사용한다면:
|
||||
- Widgets 레이어를 flat routing의 Pages 레이어처럼 사용 가능
|
||||
- 완전한 라우터 블록 생성 (데이터 fetching, loading states, error boundaries 포함)
|
||||
- 페이지 레이아웃을 이 레이어에 저장 가능
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **독립성 유지**: 각 위젯은 독립적으로 작동 가능해야 합니다
|
||||
2. **적절한 크기**: 너무 작은 컴포넌트는 `shared/ui`에, 재사용되지 않으면 `pages`에 배치
|
||||
3. **응집력**: 관련된 features와 entities를 의미 있게 조합
|
||||
4. **재사용 최적화**: 여러 페이지에서 동일한 방식으로 사용될 수 있도록 설계
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import presetConfig from "../../tailwind.config.preset";
|
||||
|
||||
const config: Config = {
|
||||
presets: [presetConfig],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./shared/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./entities/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./features/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./widgets/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
# Template Application
|
||||
|
||||
Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **프레임워크**: Next.js 15 + TypeScript
|
||||
- **스타일링**: Tailwind CSS
|
||||
- **상태관리**: Zustand
|
||||
- **데이터 페칭**: TanStack Query (React Query)
|
||||
- **폼 관리**: React Hook Form + Zod
|
||||
- **테이블**: TanStack Table
|
||||
- **HTTP 클라이언트**: Axios
|
||||
- **날짜 처리**: Day.js
|
||||
|
||||
## 프로젝트 구조 (FSD + Next.js App Router)
|
||||
|
||||
이 프로젝트는 **Feature-Sliced Design v2.1**과 **FSD 공식 Next.js 가이드**에 맞춰 구성되어 있습니다. Next.js `app/`은 라우트만 담당하고, 실제 레이어는 `src/` 아래에 있습니다.
|
||||
|
||||
```
|
||||
_template/
|
||||
├── app/ # Next.js App Router (루트) — 라우트·레이아웃 진입점만
|
||||
│ ├── layout.tsx # 루트 레이아웃 (src/app에서 providers, styles import)
|
||||
│ └── (with-base-layout)/
|
||||
│ ├── layout.tsx # BaseLayout 래퍼 (src/app/layouts에서 import)
|
||||
│ └── page.tsx # re-export: export { MainPage as default } from '@/pages/home'
|
||||
│
|
||||
├── pages/ # 빈 폴더 + README (Next.js가 src/pages를 Pages Router로 쓰지 않도록)
|
||||
│ └── README.md
|
||||
│
|
||||
└── src/ # FSD 레이어 (실제 코드)
|
||||
├── app/ # App 레이어 (슬라이스 없음, 세그먼트만)
|
||||
│ ├── providers/ # 전역 Providers (QueryClient 등)
|
||||
│ ├── styles/ # 글로벌 스타일 (globals.css)
|
||||
│ └── layouts/ # 공통 레이아웃 컴포넌트 (BaseLayoutWrapper)
|
||||
│
|
||||
├── pages/ # Pages 레이어 (페이지 슬라이스)
|
||||
│ └── home/
|
||||
│ ├── ui/
|
||||
│ └── index.ts # Public API
|
||||
│
|
||||
├── widgets/ # Widgets 레이어
|
||||
├── features/ # Features 레이어
|
||||
├── entities/ # Entities 레이어
|
||||
└── shared/ # Shared 레이어
|
||||
├── ui/
|
||||
├── api/
|
||||
├── lib/
|
||||
└── config/
|
||||
```
|
||||
|
||||
### 계층 구조 이해하기
|
||||
|
||||
FSD는 **3단계 계층**으로 구성됩니다:
|
||||
|
||||
1. **Layers (레이어)**: 책임과 의존성에 따른 수평적 분리 (app, pages, widgets, features, entities, shared)
|
||||
2. **Slices (슬라이스)**: 비즈니스 도메인에 따른 수직적 분리 (user, product, auth-login 등)
|
||||
3. **Segments (세그먼트)**: 기술적 목적에 따른 분리 (ui, api, model, lib 등)
|
||||
|
||||
> 📚 각 레이어의 자세한 설명은 해당 폴더의 README.md를 참고하세요.
|
||||
|
||||
## FSD 레이어 설명
|
||||
|
||||
FSD는 계층적 아키텍처로, 각 레이어는 책임의 범위와 의존성의 정도에 따라 구분됩니다. 7개의 표준화된 레이어가 있으며, 프로젝트에 필요한 레이어만 사용하면 됩니다.
|
||||
|
||||
### 레이어 계층 구조 (위에서 아래로)
|
||||
|
||||
```
|
||||
app/ ↑ 가장 높은 책임, 가장 많은 의존성
|
||||
pages/ |
|
||||
widgets/ |
|
||||
features/ |
|
||||
entities/ |
|
||||
shared/ ↓ 가장 낮은 책임, 의존성 없음
|
||||
```
|
||||
|
||||
### 1. App
|
||||
- **목적**: 앱 전체에 영향을 주는 기술적/비즈니스적 사항
|
||||
- **내용**: Context providers, 글로벌 스타일, 스토어 설정, 라우터 설정, 애널리틱스
|
||||
- **특징**: 슬라이스가 없고 세그먼트로만 구성
|
||||
- **의존성**: 모든 하위 레이어 사용 가능
|
||||
|
||||
### 2. Pages (src/pages)
|
||||
- **목적**: 웹사이트나 애플리케이션을 구성하는 페이지/화면
|
||||
- **내용**: 페이지 UI, 로딩 상태, 에러 바운더리, 데이터 fetching
|
||||
- **특징**: 한 페이지당 한 슬라이스 (유사한 페이지는 그룹화 가능). Next.js `app/`의 route 파일에서 re-export
|
||||
- **의존성**: `widgets`, `features`, `entities`, `shared` 사용 가능
|
||||
|
||||
> 💡 **FSD v2.1 권장사항**: "pages-first" 접근 방식을 따라, 재사용되지 않는 큰 UI 블록과 비즈니스 로직은 페이지 슬라이스에 유지하세요.
|
||||
|
||||
### 3. Widgets
|
||||
- **목적**: 큰 규모의 독립적인 UI 블록
|
||||
- **내용**: 여러 features와 entities를 조합한 복합 UI
|
||||
- **사용 시기**: 여러 페이지에서 재사용되거나, 페이지에 여러 개의 큰 독립적 블록이 있을 때
|
||||
- **의존성**: `features`, `entities`, `shared` 사용 가능 (다른 `widgets` 불가)
|
||||
|
||||
### 4. Features
|
||||
- **목적**: 사용자가 수행하는 주요 상호작용과 비즈니스 기능
|
||||
- **내용**: 사용자 시나리오 (예: 로그인, 상품 검색, 댓글 작성)
|
||||
- **사용 시기**: 여러 페이지에서 재사용되는 기능
|
||||
- **의존성**: `entities`, `shared` 사용 가능 (다른 `features` 불가)
|
||||
|
||||
### 5. Entities
|
||||
- **목적**: 비즈니스가 다루는 실제 개념 (User, Product, Order 등)
|
||||
- **내용**: 데이터 모델, 타입, API 요청, 엔티티의 시각적 표현
|
||||
- **특징**: 비즈니스 용어로 명명
|
||||
- **의존성**: 다른 `entities`, `shared` 사용 가능
|
||||
|
||||
### 6. Shared
|
||||
- **목적**: 애플리케이션의 기반 제공
|
||||
- **내용**: API 클라이언트, UI Kit, 유틸리티 라이브러리, 글로벌 설정
|
||||
- **특징**: 비즈니스 로직 없음, 슬라이스가 없고 세그먼트로만 구성
|
||||
- **의존성**: 외부 라이브러리만 사용 가능 (다른 레이어 불가)
|
||||
|
||||
## 시작하기
|
||||
|
||||
```bash
|
||||
# 의존성 설치
|
||||
pnpm install
|
||||
|
||||
# 개발 서버 실행
|
||||
pnpm run dev
|
||||
|
||||
# 프로덕션 빌드
|
||||
pnpm run build
|
||||
|
||||
# 프로덕션 서버 실행
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 환경 변수
|
||||
|
||||
`.env.local` 파일 생성:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||
```
|
||||
|
||||
## FSD 핵심 규칙
|
||||
|
||||
### 1. Import 규칙
|
||||
|
||||
> **슬라이스 내의 모듈(파일)은 오직 하위 레이어에 있는 슬라이스만 import할 수 있습니다.**
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import (하위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { User } from "entities/user";
|
||||
import { Button } from "shared/ui/button";
|
||||
|
||||
// ❌ 잘못된 import (같은 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { PostForm } from "features/post-create"; // 금지!
|
||||
|
||||
// ❌ 잘못된 import (상위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { Header } from "widgets/header"; // 금지!
|
||||
```
|
||||
|
||||
### 2. Public API 규칙
|
||||
|
||||
> **슬라이스 외부에서는 Public API(index.ts)를 통해서만 접근해야 합니다.**
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import (Public API 사용)
|
||||
import { LoginForm } from "features/auth-login";
|
||||
|
||||
// ❌ 잘못된 import (내부 구조 직접 접근)
|
||||
import { LoginForm } from "features/auth-login/ui/login-form";
|
||||
```
|
||||
|
||||
**Public API 작성 시 주의사항**:
|
||||
- 와일드카드 re-export(`export * from`) 사용 지양
|
||||
- 필요한 것만 명시적으로 export
|
||||
- 슬라이스의 인터페이스를 명확하게 표현
|
||||
|
||||
```typescript
|
||||
// ❌ 나쁜 예시
|
||||
export * from "./ui/Comment";
|
||||
export * from "./model/comments";
|
||||
|
||||
// ✅ 좋은 예시
|
||||
export { Comment } from "./ui/comment";
|
||||
export { CommentForm } from "./ui/comment-form";
|
||||
export { useComments } from "./model/use-comments";
|
||||
export type { Comment, CommentFormData } from "./model/types";
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { ApprovalLineManagementPage as default } from "@/pages/approval-line-management";
|
||||
@@ -1 +0,0 @@
|
||||
export { ChecklistManagementPage as default } from "@/pages/checklist-management";
|
||||
@@ -1 +0,0 @@
|
||||
export { CodeManagementPage as default } from "@/pages/code-management";
|
||||
@@ -1,3 +0,0 @@
|
||||
import { DashboardPage } from "@/pages/dashboard/ui/dashboard-page";
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DesignEditPage } from "@/pages/design-cnstn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <DesignEditPage id={id} />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DesignDetailGate } from "@/pages/design-cnstn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <DesignDetailGate id={id} />;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { DesignListPage } from "@/pages/design-cnstn";
|
||||
|
||||
export default DesignListPage;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { DesignRegistPage } from "@/pages/design-cnstn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/design-cnstn/lib/design-cnstn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
|
||||
return (
|
||||
<DesignRegistPage
|
||||
defaults={{
|
||||
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { EsntlAtchFileMngPage as default } from "@/pages/esntl-atch-file-mng";
|
||||
@@ -1,10 +0,0 @@
|
||||
import { FnrsEditPage } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <FnrsEditPage id={id} />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { FnrsDetailGate } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <FnrsDetailGate id={id} />;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { FnrsListPage } from "@/pages/fnrs-cnsltn";
|
||||
|
||||
export default FnrsListPage;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { FnrsRegistPage } from "@/pages/fnrs-cnsltn";
|
||||
import { parseTaskSeComCdFromApi } from "@/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = parseTaskSeComCdFromApi(params.taskSeComCd);
|
||||
|
||||
return (
|
||||
<FnrsRegistPage
|
||||
defaults={{
|
||||
taskSeComCd: taskSeComCd.length ? taskSeComCd : undefined,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { InstlEditPage } from "@/pages/instl-sncn";
|
||||
|
||||
interface InstlSncnEditRouteProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params }: InstlSncnEditRouteProps) {
|
||||
const { id } = await params;
|
||||
return <InstlEditPage id={id} />;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { InstlSncnDetailGate } from "@/pages/instl-sncn";
|
||||
|
||||
export default async function InstlSncnDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return <InstlSncnDetailGate id={id} />;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { InstlListPage } from "@/pages/instl-sncn";
|
||||
|
||||
export default InstlListPage;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { InstlRegistPage } from "@/pages/instl-sncn";
|
||||
|
||||
interface InstlSncnRegistRouteProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: InstlSncnRegistRouteProps) {
|
||||
const params = await searchParams;
|
||||
const taskSeComCd = params.taskSeComCd
|
||||
? params.taskSeComCd.split(",").filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<InstlRegistPage
|
||||
defaults={{
|
||||
taskSeComCd,
|
||||
admdstCd: params.admdstCd,
|
||||
sido: params.sido,
|
||||
gugun: params.gugun,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { BaseLayoutWrapper } from "@/app/layouts/base-layout";
|
||||
|
||||
interface WithBaseLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function WithBaseLayout({ children }: WithBaseLayoutProps) {
|
||||
return <BaseLayoutWrapper>{children}</BaseLayoutWrapper>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { MyPbadmsSprtListPage } from "@/pages/my-pbadms-sprt";
|
||||
|
||||
export default MyPbadmsSprtListPage;
|
||||
@@ -1,3 +0,0 @@
|
||||
import { NtiHstryListPage } from "@/pages/nti-hstry";
|
||||
|
||||
export default NtiHstryListPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { RvwMnpwMngPage as default } from "@/pages/rvw-mnpw-mng";
|
||||
@@ -1,13 +0,0 @@
|
||||
import { SewerBscPlanEditPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ chgCycl?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanEditPage id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { SewerBscPlanDetailGate } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ chgCycl?: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { id } = await params;
|
||||
const { chgCycl } = await searchParams;
|
||||
const propChgCycl = chgCycl ? Number(chgCycl) : undefined;
|
||||
return <SewerBscPlanDetailGate id={id} propChgCycl={propChgCycl} />;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { SewerBscPlanListPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
export default SewerBscPlanListPage;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { SewerBscPlanRegistPage } from "@/pages/sewer-bsc-plan";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
sewerBscPlanSeComCd?: string;
|
||||
sewerBscPlanSeCdNm?: string;
|
||||
admdstCd?: string;
|
||||
sido?: string;
|
||||
gugun?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const params = await searchParams;
|
||||
|
||||
return <SewerBscPlanRegistPage defaults={params} />;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { Providers } from "@/app/providers";
|
||||
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
description: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
return (
|
||||
<html lang="ko" data-theme={theme?.value}>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
# App Layer
|
||||
|
||||
**App**은 애플리케이션 전체에 영향을 미치는 기술적, 비즈니스적 사항을 다루는 최상위 레이어입니다.
|
||||
|
||||
## 목적
|
||||
|
||||
- 애플리케이션의 진입점(entrypoint) 제공
|
||||
- 전역 Context Providers 설정
|
||||
- 라우터 구성
|
||||
- 글로벌 스타일 정의
|
||||
- 전역 상태 저장소 설정
|
||||
- 애널리틱스 및 모니터링 설정
|
||||
|
||||
## 특징
|
||||
|
||||
- **슬라이스 없음**: App 레이어는 Shared처럼 슬라이스가 없고 세그먼트로만 구성됩니다
|
||||
- **전체 앱 관심사**: 특정 비즈니스 도메인이 아닌 앱 전체와 관련된 코드만 포함
|
||||
- **최상위 레이어**: 모든 하위 레이어의 모듈을 자유롭게 import 가능
|
||||
|
||||
## 구조 (Next.js App Router)
|
||||
|
||||
Next.js를 사용하는 경우, App 레이어는 주로 `app/` 디렉토리로 표현됩니다:
|
||||
|
||||
```
|
||||
app/
|
||||
├── (routes)/ # 라우트 그룹 및 페이지
|
||||
│ ├── (auth)/
|
||||
│ │ ├── login/
|
||||
│ │ │ └── page.tsx
|
||||
│ │ └── register/
|
||||
│ │ └── page.tsx
|
||||
│ └── (main)/
|
||||
│ ├── dashboard/
|
||||
│ │ └── page.tsx
|
||||
│ └── settings/
|
||||
│ └── page.tsx
|
||||
├── layout.tsx # 루트 레이아웃
|
||||
├── page.tsx # 홈 페이지
|
||||
├── globals.css # 글로벌 스타일
|
||||
├── providers.tsx # 전역 Providers 설정
|
||||
└── error.tsx # 글로벌 에러 핸들러
|
||||
```
|
||||
|
||||
### 일반적인 FSD 세그먼트 구조
|
||||
|
||||
FSD 표준 세그먼트를 사용하는 경우:
|
||||
|
||||
```
|
||||
app/
|
||||
├── entrypoint/ # 애플리케이션 진입점 (프레임워크별)
|
||||
│ └── index.tsx
|
||||
├── providers/ # Context Providers
|
||||
│ ├── query-provider.tsx
|
||||
│ ├── theme-provider.tsx
|
||||
│ └── index.tsx
|
||||
├── styles/ # 글로벌 스타일
|
||||
│ ├── globals.css
|
||||
│ └── theme.css
|
||||
├── store/ # 글로벌 상태 저장소 설정
|
||||
│ └── index.ts
|
||||
└── routes/ # 라우터 설정
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
## 예시
|
||||
|
||||
### Next.js Providers 설정
|
||||
|
||||
```typescript
|
||||
// app/providers.tsx
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
import { Providers } from './providers';
|
||||
import './globals.css';
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 글로벌 설정
|
||||
|
||||
```typescript
|
||||
// app/config/analytics.ts
|
||||
export function initAnalytics() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// 애널리틱스 초기화
|
||||
}
|
||||
}
|
||||
|
||||
// app/config/monitoring.ts
|
||||
export function initMonitoring() {
|
||||
// 에러 모니터링 설정
|
||||
}
|
||||
```
|
||||
|
||||
## Next.js App Router와 FSD
|
||||
|
||||
Next.js의 `app/` 디렉토리는 FSD의 **App 레이어**와 **Pages 레이어**를 동시에 담당합니다:
|
||||
|
||||
- **루트 레벨 파일들** (`layout.tsx`, `providers.tsx`, `globals.css`): App 레이어
|
||||
- **라우트 폴더들** (`dashboard/`, `login/` 등): Pages 레이어
|
||||
|
||||
각 페이지별 비즈니스 로직은:
|
||||
- 재사용되지 않는 UI는 페이지 슬라이스(`app/dashboard/` 등) 안에 유지
|
||||
- 재사용되는 기능은 `features/` 레이어로 분리
|
||||
- 여러 페이지에서 재사용되는 큰 UI 블록은 `widgets/` 레이어로 분리
|
||||
|
||||
## 레이어 의존성 규칙
|
||||
|
||||
✅ **허용되는 의존성**:
|
||||
- 모든 하위 레이어 (`pages`, `widgets`, `features`, `entities`, `shared`)
|
||||
- 같은 `app` 레이어 내의 다른 세그먼트
|
||||
- 외부 라이브러리
|
||||
|
||||
❌ **금지된 의존성**:
|
||||
- 없음 (App은 최상위 레이어)
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **비대화 방지**: App 레이어가 너무 커지면 관리가 어려워집니다. 대부분의 로직은 하위 레이어에 배치하세요
|
||||
2. **전역 관심사만**: 특정 페이지나 기능에만 관련된 코드는 해당 레이어로 이동
|
||||
3. **라우터 레벨 로직**: 라우터 및 서버 레벨 로직은 이 레이어에 유지
|
||||
4. **Processes 레이어 사용 지양**: Processes 레이어는 deprecated되었으므로 해당 로직은 `app`이나 `features`에 배치
|
||||
|
||||
## FSD v2.1 권장사항
|
||||
|
||||
최신 FSD 버전(v2.1)은 **"pages-first"** 접근 방식을 권장합니다:
|
||||
|
||||
- 더 많은 코드를 페이지 슬라이스에 유지
|
||||
- 큰 UI 블록과 재사용되지 않는 비즈니스 로직은 즉시 추출하지 말고 페이지에 보관
|
||||
- 실제로 재사용이 필요할 때만 `features`나 `widgets`로 분리
|
||||
|
||||
이 접근 방식은 과도한 추상화를 방지하고 코드를 더 쉽게 탐색할 수 있게 합니다.
|
||||
@@ -1,10 +0,0 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config";
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN });
|
||||
return NextResponse.json({ message: "Logged out" }, { status: 200 });
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
COOKIE_DOMAIN,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
params: { url: string[] },
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
) {
|
||||
const cookieStore = await cookies();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
const deviceId = request.headers.get("X-Device-Id");
|
||||
if (deviceId) {
|
||||
headers["X-Device-Id"] = deviceId;
|
||||
}
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
|
||||
// silent refresh
|
||||
if (isRefreshHandler) {
|
||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||
|
||||
if (!headers[CLAIM_NAME]) {
|
||||
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isRefreshHandler) {
|
||||
body = await request.json();
|
||||
}
|
||||
|
||||
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
|
||||
const result = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
|
||||
}
|
||||
|
||||
const accessToken = result.headers.get(ACCESS_TOKEN);
|
||||
const refreshToken = result.headers.get(REFRESH_TOKEN);
|
||||
const userId = result.headers.get(AUTH_USER_ID);
|
||||
|
||||
if (accessToken && userId) {
|
||||
const response = NextResponse.json({
|
||||
[ACCESS_TOKEN]: accessToken,
|
||||
[AUTH_USER_ID]: userId,
|
||||
});
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
domain: COOKIE_DOMAIN,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
|
||||
} catch (error) {
|
||||
console.error(`[Proxy API] Error caught in handler:`, error);
|
||||
return NextResponse.json({ message: "Server Error", error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request, props: { params: Promise<{ url: string[] }> }) {
|
||||
const params = await props.params;
|
||||
return handler(request, params, "POST");
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, props: { params: Promise<{ url: string[] }> }) {
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
export default function GlobalNotFound() {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
|
||||
<div className="mb-16 text-center">
|
||||
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
|
||||
<span className="text-primary">디지털 행정지원 시스템</span>에 오신 것을 환영합니다.
|
||||
</h1>
|
||||
<p className="mt-6 text-[2rem] text-[#666]">오류가 발생하였습니다.</p>
|
||||
</div>
|
||||
|
||||
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
|
||||
<Image
|
||||
src="/images/magnifying-glass-with-warning.png"
|
||||
alt=""
|
||||
width={280}
|
||||
height={280}
|
||||
className="size-[28rem] object-contain"
|
||||
/>
|
||||
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">페이지가 존재하지 않습니다.</p>
|
||||
<p className="mt-2 text-[1.6rem] text-[#666]">관리자에게 문의하여 주십시오</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,800 +0,0 @@
|
||||
# 실무협의문서 개발 가이드
|
||||
|
||||
> **대상 앱**: `frontend/apps/as`
|
||||
> **작성일**: 2026-08-27
|
||||
> **버전**: 1.0
|
||||
> **SoT (백엔드)**:
|
||||
>
|
||||
> - `PwrkCnsltnDocCrtRsltController` — `/api/v1/pwrk-cnsltn-doc-crt-rslt`
|
||||
> - `PwrkCnsltnMngController` — `/api/v1/pwrk-cnsltn-mng`
|
||||
> - `DcRagApiToController` — `/api/v1/dcrag-to`
|
||||
> - 초안생성 데이터 설명 (`02.03.초안생성_데이터_설명.md`)
|
||||
>
|
||||
> **현재 프론트 상태**: `pwrk-cnsltn-`\* 엔티티·화면 **미구현**. 재원협의(`fnrs-cnsltn`)만 존재한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
재원협의 접수자가 **자료 검증 → 실무협의서 초안 생성 → 한글 웹기안기 열람/다운로드 → 편집본 버전 저장**까지 구현할 때 사용하는 프론트엔드 개발 가이드이다.
|
||||
|
||||
백엔드 컨트롤러·DTO를 **API source of truth**로 두고, 초안생성 데이터 설명의 업무 시퀀스를 화면 동작에 매핑한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 도메인 개요
|
||||
|
||||
실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 문서번호 규칙:
|
||||
|
||||
| 문서 | 번호 예시 | 필드 |
|
||||
| -------- | ----------------- | ----------------------------- |
|
||||
| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` |
|
||||
| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` |
|
||||
|
||||
재원협의 1건당 실무협의서는 **1건만** 생성된다. `POST /pwrk-cnsltn-mng`는 `upDocNo`로 기존 문서를 재사용하고, 없으면 문서번호를 발급한다.
|
||||
|
||||
### 2-1. 핵심 식별자
|
||||
|
||||
| 필드 | 의미 | 비고 |
|
||||
| ----------------- | ------------------------------ | --------------------------------------------------------- |
|
||||
| `pwrkCnsltnDocNo` | 실무협의문서번호 | 문서 PK. `P-YYYYMMDD-NNNN` |
|
||||
| `upDocNo` | 상위문서번호(재원협의) | `upDocSeComCd = "FNRS"` |
|
||||
| `crtDmndUuid` | 생성요청 UUID | 초안 생성 작업·결과·다운로드의 키 |
|
||||
| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` |
|
||||
| `orgnlOtptDocCn` | 원본출력문서내용(HWP 바이너리) | **목록/상세 응답에서는 항상** `null`. 다운로드 API만 제공 |
|
||||
|
||||
### 2-2. IIPBS 약어
|
||||
|
||||
| 한글 | 약어 | 필드 예 |
|
||||
| -------- | ------ | ----------------------------- |
|
||||
| 실무 | PWRK | `pwrkCnsltnDocNo` |
|
||||
| 협의 | CNSLTN | `pwrk-cnsltn-mng` |
|
||||
| 관리 | MNG | `PwrkCnsltnMng` |
|
||||
| 문서 | DOC | `pwrkCnsltnDocNo` |
|
||||
| 생성 | CRT | `crtDmndUuid`, `crtSttsComCd` |
|
||||
| 요청 | DMND | `crtDmndUuid` |
|
||||
| 결과 | RSLT | `crtRslt` |
|
||||
| 버전 | VER | `verSn` |
|
||||
| 일련번호 | SN | `verSn` |
|
||||
| 원본 | ORGNL | `orgnlOtptDocCn` |
|
||||
| 출력 | OTPT | `orgnlOtptDocCn` |
|
||||
| 내용 | CN | `orgnlOtptDocCn` |
|
||||
| 완료 | CMPTN | `crtCmptnDt` |
|
||||
| 일시 | DT | `crtCmptnDt` |
|
||||
| 검증 | VRFC | `vrfcSttsComCd` |
|
||||
| 전송 | TRSM | `trsmFileList` |
|
||||
| 보완 | SPLMNT | `splmntDmndList` |
|
||||
| 요청자 | RQSTR | `rqstrUuid`, `rqstrNm` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 전체 업무 흐름
|
||||
|
||||
재원협의 **접수자**가 실무협의서 초안을 만드는 경로이다. 프론트는 DC RAG를 직접 호출하지 않고 **as-service**만 호출한다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant User as 재원협의 접수자
|
||||
participant FE as AS Frontend
|
||||
participant AS as as-service
|
||||
participant RAG as DC RAG
|
||||
|
||||
User->>FE: [자료 검증 요청]
|
||||
FE->>AS: POST /dcrag-to/documents/transfer<br/>{ fnrsCnsltnDocNo }
|
||||
AS->>RAG: 자료전송 (IF-01)
|
||||
AS-->>FE: { resultCode: SUCCESS, requestId }
|
||||
Note over FE: 재원협의 상세 재조회
|
||||
AS-->>FE: esntlFileList / etcFileList 에<br/>vrfcDt, vrfcRsltCn, vrfcSttsComCd
|
||||
|
||||
User->>FE: [초안 생성]
|
||||
FE->>AS: POST /pwrk-cnsltn-mng<br/>{ upDocNo, upDocSeComCd: FNRS, prgrsSttsComCd: DV }
|
||||
alt 실무협의서 번호 없음
|
||||
AS->>AS: P-번호 발급 + 문서 등록
|
||||
else 이미 존재
|
||||
AS->>AS: 기존 pwrkCnsltnDocNo 재사용
|
||||
end
|
||||
AS->>RAG: 초안생성 비동기 요청 (IF-03-1)
|
||||
AS-->>FE: 201 { pwrkCnsltnDocNo, crtDmndUuid, crtSttsComCd: R, estimatedMinutes: 10 }
|
||||
|
||||
loop 약 10분, status !== COMPLETED/FAILED
|
||||
FE->>AS: GET /dcrag-to/documents/draft/status/{crtDmndUuid}
|
||||
AS-->>FE: { status, events[], progressPercent }
|
||||
end
|
||||
|
||||
Note over FE: events 에 COMPLETED 가 있으면<br/>웹기안기 열기 / 다운로드 가능
|
||||
|
||||
User->>FE: [문서 열기] 또는 [다운로드]
|
||||
FE->>AS: GET /pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open<br/>또는 /download
|
||||
|
||||
User->>FE: 웹기안기 편집 후 [저장]
|
||||
FE->>AS: POST /pwrk-cnsltn-doc-crt-rslt (multipart)<br/>crtDmndUuid + file
|
||||
AS-->>FE: 201 { verSn: N+1, ... }
|
||||
```
|
||||
|
||||
### 3-1. 프론트가 호출하면 안 되는 API
|
||||
|
||||
| API | 이유 |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `POST /dcrag-to/documents/draft/generate` | 초안 생성은 `POST /pwrk-cnsltn-mng`가 문서번호 발급 + DC RAG 호출을 **한 트랜잭션**으로 처리한다. 프론트는 관리 API만 호출한다. |
|
||||
|
||||
---
|
||||
|
||||
## 4. 화면 데이터 흐름
|
||||
|
||||
초안 생성은 **재원협의 상세**에서 시작하고, 실무협의 목록/상세는 별도 화면이다.
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// ===== 재원협의 상세 (기존 fnrs-cnsltn view) =====
|
||||
CALL findById(fnrsCnsltnDocNo)
|
||||
DISPLAY 재원협의 상세 + 필수/기타 첨부
|
||||
|
||||
ON [자료 검증 요청] 버튼 CLICK DO
|
||||
CALL requestDocumentTransfer({ fnrsCnsltnDocNo })
|
||||
CALL findById(fnrsCnsltnDocNo) // 검증결과 필드 반영
|
||||
DISPLAY 첨부 리스트 (vrfcSttsComCd: PASS | FAIL)
|
||||
END ON
|
||||
|
||||
ON [초안 생성] 버튼 CLICK DO
|
||||
CALL pwrkCnsltnMngService.save({
|
||||
upDocNo: fnrsCnsltnDocNo,
|
||||
upDocSeComCd: "FNRS",
|
||||
prgrsSttsComCd: "DV"
|
||||
})
|
||||
// → pwrkCnsltnDocNo, crtDmndUuid, estimatedMinutes
|
||||
START POLL getDraftGenerationStatus(crtDmndUuid)
|
||||
DISPLAY 초안 생성 진행률 (약 10분)
|
||||
END ON
|
||||
|
||||
ON POLL status = COMPLETED 또는 events.eventType 에 COMPLETED DO
|
||||
ENABLE [웹기안기 열기] [다운로드]
|
||||
END ON
|
||||
|
||||
ON [웹기안기 열기] 버튼 CLICK DO
|
||||
CALL HwpCtrl.Open( webhwpOpenUrl(crtDmndUuid) ) // TBD: 컨트롤러 미구현
|
||||
END ON
|
||||
|
||||
ON [다운로드] 버튼 CLICK DO
|
||||
CALL downloadLatest(crtDmndUuid) // blob + Content-Disposition
|
||||
SAVE 로컬 파일
|
||||
END ON
|
||||
|
||||
// ===== 실무협의 목록 =====
|
||||
CALL pwrkCnsltnMngService.findPage(searchDto + pageable)
|
||||
DISPLAY 실무협의 문서 그리드
|
||||
|
||||
ON 행 CLICK DO
|
||||
NAVIGATE 실무협의 상세 (pwrkCnsltnDocNo)
|
||||
END ON
|
||||
|
||||
// ===== 실무협의 상세 / 재원협의에서 연계 조회 =====
|
||||
CALL findById(pwrkCnsltnDocNo)
|
||||
// 또는 재원협의 번호로: findByUpDocNo(fnrsCnsltnDocNo)
|
||||
DISPLAY 마스터 + cnsltnList + pjtcoAjmtList + crtDmndList.crtRslt
|
||||
// crtRslt.orgnlOtptDocCn 은 항상 null — 바이너리는 download API
|
||||
|
||||
ON 버전 이력 조회 DO
|
||||
CALL pwrkCnsltnDocCrtRsltService.findPage({ crtDmndUuid, sort: "verSn,desc" })
|
||||
DISPLAY 버전 목록
|
||||
END ON
|
||||
|
||||
ON 웹기안기 편집 완료 [저장] DO
|
||||
CALL saveMultipart({ crtDmndUuid, file }) // 서버가 verSn + 1
|
||||
INVALIDATE 상세·버전 목록
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 명세
|
||||
|
||||
### 5-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| ----------- | ----------------------------------------------------------------- |
|
||||
| Gateway | `/as-service` |
|
||||
| Base prefix | `ENDPOINT.AS_SERVICE.*` (`apps/as/src/shared/config/api.ts`) |
|
||||
| Auth 헤더 | `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터) |
|
||||
| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
추가할 ENDPOINT:
|
||||
|
||||
```ts
|
||||
AS_SERVICE: {
|
||||
// 기존 …
|
||||
PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng",
|
||||
PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt",
|
||||
DCRAG_TO: "/as-service/api/v1/dcrag-to",
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5-2. DC RAG 연계 (`DcRagApiToController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/dcrag-to/`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ----------------- | -------------------------- | ------ | ------------------------------------------------ | --------------------------------- | ------------------------------------- |
|
||||
| 자료전송요청 | `requestDocumentTransfer` | POST | `/dcrag-to/documents/transfer` | `DcRagDocumentTransferRequestDto` | `DcRagCommonResponseDto` |
|
||||
| 초안생성 진행상태 | `getDraftGenerationStatus` | GET | `/dcrag-to/documents/draft/status/{crtDmndUuid}` | path | `DcRagDraftStatusResponseDto` |
|
||||
| 금액검증요청 | `requestCostVerification` | POST | `/dcrag-to/documents/cost/verify` | `DcRagCostVerifyRequestDto` | `DcRagCommonResponseDto` (202) |
|
||||
| 초안생성요청 | `requestDraftGeneration` | POST | `/dcrag-to/documents/draft/generate` | `DcRagDraftGenerateRequestDto` | `DcRagDraftGenerateResponseDto` (202) |
|
||||
|
||||
**화면에서 쓰는 것**: 자료전송 + 상태 폴링 (+ 추후 금액검증).
|
||||
초안생성요청은 5-3의 `pwrkCnsltnMngService.save`로 대체한다.
|
||||
|
||||
#### 자료전송 Request / Response
|
||||
|
||||
```json
|
||||
POST /as-service/api/v1/dcrag-to/documents/transfer
|
||||
{ "fnrsCnsltnDocNo": "F-20260824-0001" }
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"resultCode": "SUCCESS",
|
||||
"message": "자료전송요청 접수 완료",
|
||||
"requestId": "F-20260824-0001"
|
||||
}
|
||||
```
|
||||
|
||||
자료 검증이 **완료된 뒤** 재원협의 상세를 다시 조회하면, 필수첨부(`esntlFileList`)·기타첨부(`etcFileList`) 각 파일에 검증 결과가 붙는다.
|
||||
|
||||
| 필드 | 예 | 의미 |
|
||||
| --------------- | ----------------------------- | ------------ |
|
||||
| `vrfcDt` | `2026-08-25 18:13:49` | 검증일시 |
|
||||
| `vrfcRsltCn` | `정상 확인 (재원협의 신청서)` | 검증결과내용 |
|
||||
| `vrfcSttsComCd` | `PASS` / `FAIL` | 검증상태 |
|
||||
|
||||
프론트 `FnrsCnsltnMngDetResponseDto` / `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 위 3필드가 **아직 없다**. 재원협의 상세 첨부 영역에 배지를 그리려면 타입을 확장해야 한다.
|
||||
|
||||
#### 초안생성 상태 폴링
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/dcrag-to/documents/draft/status/{crtDmndUuid}
|
||||
```
|
||||
|
||||
| 필드 | 설명 |
|
||||
| ----------------------- | --------------------------------------------------------------------------------- |
|
||||
| `status` | `REQUESTED` / `DOWNLOADING` / `ANALYZING` / `GENERATING` / `COMPLETED` / `FAILED` |
|
||||
| `statusName` | 표시명 (예: `생성 완료`) |
|
||||
| `progressPercent` | 0~100 |
|
||||
| `events[]` | `{ eventType, eventMessage, occurredDt }` |
|
||||
| `startedDt` | 시작일시 |
|
||||
| `estimatedCompletionDt` | 예상 완료일시 |
|
||||
| `errorMessage` | 실패 시 메시지 |
|
||||
|
||||
폴링 규칙:
|
||||
|
||||
- 주기: **5~10초** (TBD, UX 확정).
|
||||
- 예상 소요: **약 10분** (`estimatedMinutes`).
|
||||
- 종료 조건: `status === "COMPLETED"` **또는** `events`에 `eventType === "COMPLETED"`.
|
||||
- 실패: `status === "FAILED"` → 폴링 중단, `errorMessage` 표시.
|
||||
- 완료 전에는 웹기안기 열기·다운로드 버튼을 **비활성**.
|
||||
|
||||
이벤트 순서 예시:
|
||||
|
||||
| eventType | eventMessage |
|
||||
| ------------- | --------------------------- |
|
||||
| `REQUESTED` | 요청을 접수했습니다. |
|
||||
| `DOWNLOADING` | 신청서를 수신하는 중입니다. |
|
||||
| `ANALYZING` | 자료 준비가 완료되었습니다. |
|
||||
| `GENERATING` | 문서 저장이 완료되었습니다. |
|
||||
| `COMPLETED` | 문서 생성이 완료되었습니다. |
|
||||
|
||||
---
|
||||
|
||||
### 5-3. 실무협의관리 (`PwrkCnsltnMngController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-mng/`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ------------------- | ---------------------- | ------ | ----------------------------------------------------- | ----------------------------------- | ------------------------------------ |
|
||||
| 페이징 조회 | `findPage` | GET | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSearchDto` + Pageable | `Page<PwrkCnsltnMngResponseDto>` |
|
||||
| 상세 (문서번호) | `findById` | GET | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
|
||||
| 상세 (재원협의번호) | `findByUpDocNo` | GET | `/pwrk-cnsltn-mng/updocno/{upDocNo}` | path | `PwrkCnsltnMngDetResponseDto` |
|
||||
| 등록 + 초안생성 | `save` | POST | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSaveRequestDto` | `PwrkCnsltnMngSaveResponseDto` (201) |
|
||||
| 신청 통합 저장 | `saveApply` | POST | `/pwrk-cnsltn-mng/save-apply` | `PwrkCnsltnApplySaveRequestDto` | `string` (문서번호, 201) |
|
||||
| 수정 | `update` | PUT | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | `PwrkCnsltnMngUpdateRequestDto` | `string` |
|
||||
| 삭제 | `delete` → `remove` | DELETE | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | 204 |
|
||||
| 보완완료 제출 | `submitSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/submitsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
|
||||
| 보완요청 등록 | `submitReqSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/reqsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` |
|
||||
| 진행상태 수정 | `changePrgrsSttsComCd` | POST | `/pwrk-cnsltn-mng/doc/updatePrgrsStts` | `DocPrgrsUpdateRequestDto` | 200 |
|
||||
|
||||
목록 기본 호출:
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-mng?page=0&size=10&sort=pwrkCnsltnDocNo,desc
|
||||
```
|
||||
|
||||
#### 초안 생성 Request / Response (`save`)
|
||||
|
||||
```json
|
||||
POST /as-service/api/v1/pwrk-cnsltn-mng
|
||||
{
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260826-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV",
|
||||
"resultCode": "ACCEPTED",
|
||||
"message": "초안 생성 작업이 접수되었습니다.",
|
||||
"crtDmndUuid": "382f2821-9231-42e6-8309-4a1718a2ead8",
|
||||
"crtSttsComCd": "R",
|
||||
"requestId": "382f2821-9231-42e6-8309-4a1718a2ead8",
|
||||
"estimatedMinutes": 10
|
||||
}
|
||||
```
|
||||
|
||||
백엔드 동작 (`PwrkCnsltnMngService.save`):
|
||||
|
||||
1. `upDocNo`로 기존 실무협의서가 있으면 **그 번호를 재사용** (재원협의 1 : 실무협의 1).
|
||||
2. 없으면 `docNoMngService.issueDocNo(PWRK)`로 `P-` 번호 발급 후 insert.
|
||||
3. DC RAG 초안생성을 비동기로 호출하고 `crtDmndUuid`를 응답에 실어 반환.
|
||||
4. DC RAG 실패 시에도 **문서는 롤백하지 않고** 로그만 남긴다. 프론트는 `resultCode`가 `ACCEPTED`/`SUCCESS`가 아니면 토스트로 실패를 알린다.
|
||||
|
||||
#### 상세 응답 구조 (`PwrkCnsltnMngDetResponseDto`)
|
||||
|
||||
재원협의 상세에서 `GET .../updocno/{fnrsCnsltnDocNo}`로 연계 데이터를 한 번에 가져온다.
|
||||
|
||||
```json
|
||||
{
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"upDocNo": "F-20260824-0001",
|
||||
"upDocSeComCd": "FNRS",
|
||||
"prgrsSttsComCd": "DV",
|
||||
"cnsltnList": [],
|
||||
"pjtcoAjmtList": [],
|
||||
"crtDmndList": [
|
||||
{
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"pwrkCnsltnDocNo": "P-20260827-0001",
|
||||
"rqstrUuid": "65a00f65-8460-49af-98ec-042977e56f4b",
|
||||
"rqstrNm": "관리자",
|
||||
"crtSttsComCd": "C",
|
||||
"crtRslt": {
|
||||
"crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a",
|
||||
"verSn": 1,
|
||||
"rgtrUuid": "...",
|
||||
"rgtrNm": "관리자",
|
||||
"orgnlOtptDocCn": null,
|
||||
"crtCmptnDt": "2026-08-27 10:47:01"
|
||||
}
|
||||
}
|
||||
],
|
||||
"splmntDmndList": [],
|
||||
"trsmFileList": []
|
||||
}
|
||||
```
|
||||
|
||||
`crtRslt`는 최신 버전 메타만 포함한다. `orgnlOtptDocCn`은 절대 쓰지 말고 download API를 호출한다.
|
||||
|
||||
마스터에 재원협의 사업정보가 조인된다: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등.
|
||||
|
||||
---
|
||||
|
||||
### 5-4. 문서생성결과 (`PwrkCnsltnDocCrtRsltController`)
|
||||
|
||||
프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-doc-crt-rslt/`
|
||||
복합 PK: `crtDmndUuid` + `verSn`
|
||||
|
||||
| 기능 | Controller | Method | URL | Request | Response |
|
||||
| ------------------ | ------------------------- | ------ | ---------------------------------- | ----------------------------------------------- | --------------------------------------- |
|
||||
| 버전 이력 페이징 | `findPage` | GET | `/pwrk-cnsltn-doc-crt-rslt` | `PwrkCnsltnDocCrtRsltSearchDto` + Pageable | `Page<PwrkCnsltnDocCrtRsltResponseDto>` |
|
||||
| 특정 버전 상세 | `findById` | GET | `/{crtDmndUuid}/{verSn}` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
|
||||
| 최신 버전 상세 | `findLatestByCrtDmndUuid` | GET | `/{crtDmndUuid}/latest` | path | `PwrkCnsltnDocCrtRsltResponseDto` |
|
||||
| 특정 버전 다운로드 | `download` | GET | `/{crtDmndUuid}/{verSn}/download` | path | `byte[]` (attachment) |
|
||||
| 최신 버전 다운로드 | `downloadLatest` | GET | `/{crtDmndUuid}/download` | path | `byte[]` (attachment) |
|
||||
| 등록 (multipart) | `saveMultipart` | POST | `/` `multipart/form-data` | `PwrkCnsltnDocCrtRsltSaveRequestDto` + `file` | `PwrkCnsltnDocCrtRsltResponseDto` (201) |
|
||||
| 등록 (JSON) | `saveJson` | POST | `/` `application/json` | `PwrkCnsltnDocCrtRsltSaveRequestDto` | 201 |
|
||||
| 수정 (multipart) | `updateMultipart` | PUT | `/{crtDmndUuid}/{verSn}` multipart | `PwrkCnsltnDocCrtRsltUpdateRequestDto` + `file` | ResponseDto |
|
||||
| 수정 (JSON) | `updateJson` | PUT | `/{crtDmndUuid}/{verSn}` json | UpdateRequestDto | ResponseDto |
|
||||
| 삭제 | `delete` → `remove` | DELETE | `/{crtDmndUuid}/{verSn}` | path | 204 |
|
||||
|
||||
Spring 매핑 주의: `/{crtDmndUuid}/latest` 와 `/{crtDmndUuid}/download` 는 `/{crtDmndUuid}/{verSn}` 보다 **구체 경로가 우선**한다. 프론트 path를 섞지 말 것.
|
||||
|
||||
버전 이력 조회 예:
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc
|
||||
```
|
||||
|
||||
#### 다운로드 (attachment)
|
||||
|
||||
화면 [다운로드] 버튼 → PC 저장.
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/download
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/download
|
||||
```
|
||||
|
||||
- axios: `responseType: "blob"`.
|
||||
- 파일명: `Content-Disposition`의 `filename*=UTF-8''...` (`parseFilenameFromResponse` 재사용).
|
||||
- 서버 파일명 규칙: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`.
|
||||
- **현재 컨트롤러 Content-Type**: `application/x-hwp`.
|
||||
데이터 설명 예시는 `application/haansofthwpx`. 프론트는 Content-Type에 의존하지 말고 blob으로 저장한다.
|
||||
|
||||
기존 `attachService` 다운로드와 동일 패턴:
|
||||
|
||||
```ts
|
||||
const response = await client.get<Blob>(url, { responseType: "blob" });
|
||||
const filename = parseFilenameFromResponse(response);
|
||||
const objectUrl = window.URL.createObjectURL(response.data);
|
||||
```
|
||||
|
||||
#### 웹기안기 열기 (inline) — TBD
|
||||
|
||||
데이터 설명 기준 엔드포인트. **현재** `PwrkCnsltnDocCrtRsltController`**에는 없음.** 백엔드 추가 후 연결한다.
|
||||
|
||||
```http
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open
|
||||
GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/webhwp/open
|
||||
```
|
||||
|
||||
의도:
|
||||
|
||||
- `Content-Disposition: inline`
|
||||
- 한컴 웹기안기 `HwpCtrl.Open(url)` 이 이 URL을 직접 호출
|
||||
- 브라우저 다운로드 창이 뜨지 않고 캔버스에 로드
|
||||
|
||||
프론트는 **blob fetch 후 Object URL을 넘기지 않는다.** 인증 헤더가 필요한 절대 URL을 기안기에 전달하는 방식이므로, 쿠키/게이트웨이 인증 가능 여부를 백엔드와 확인한다 (TBD).
|
||||
|
||||
#### 신규 버전 등록 (웹기안기 저장)
|
||||
|
||||
서버가 최신 `verSn + 1`을 부여한다. 클라이언트가 `verSn`을 보내지 않는다.
|
||||
|
||||
```http
|
||||
POST /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
crtDmndUuid: {uuid}
|
||||
file: 재원협의 실무협의서_v2.hwpx
|
||||
```
|
||||
|
||||
화면에서는 **multipart만** 사용한다. JSON `saveJson`은 바이너리를 body에 넣는 경로라 UI에서 쓰지 않는다.
|
||||
|
||||
`file` part 이름: `"file"`. DTO 필드는 `@ModelAttribute` 로 평문 필드(`crtDmndUuid`) 또는 `saveDto` part.
|
||||
|
||||
---
|
||||
|
||||
## 6. DTO 매핑 (프론트 타입명 = 백엔드 클래스명)
|
||||
|
||||
### 6-1. `PwrkCnsltnDocCrtRslt*`
|
||||
|
||||
| 계층 | 파일 | 정의 |
|
||||
| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- |
|
||||
| Save Request | `pwrk-cnsltn-doc-crt-rslt-payload-schema.ts` | zod SSOT → `PwrkCnsltnDocCrtRsltSaveRequestDto` |
|
||||
| Response / Search / Update / Service | `pwrk-cnsltn-doc-crt-rslt-interface.ts` | `interface` |
|
||||
|
||||
**Response**
|
||||
|
||||
| 필드 | 타입 | 비고 |
|
||||
| ---------------- | -------- | --------------------- |
|
||||
| `crtDmndUuid` | `string` | UUID |
|
||||
| `verSn` | `number` | BigDecimal |
|
||||
| `rgtrUuid` | `string` | |
|
||||
| `rgtrNm` | `string` | 서버 enrich |
|
||||
| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 |
|
||||
| `crtCmptnDt` | `string` | `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
**Search** (`PageableParams` 확장)
|
||||
|
||||
| 필드 | 비고 |
|
||||
| ----------------- | ---------------------- |
|
||||
| `crtDmndUuid` | 버전 이력 조회 시 필수 |
|
||||
| `pwrkCnsltnDocNo` | |
|
||||
| `verSn` | |
|
||||
| `rgtrUuid` | |
|
||||
|
||||
**Save Request (zod)**
|
||||
|
||||
| 필드 | 필수 | 비고 |
|
||||
| ------------- | ---- | ------------------------------------ |
|
||||
| `crtDmndUuid` | Y | |
|
||||
| `rgtrUuid` | N | 미전송 시 서버가 로그인 사용자 사용 |
|
||||
| `file` | UI | multipart `file` part. DTO 필드 아님 |
|
||||
|
||||
**Update Request**
|
||||
|
||||
| 필드 | 비고 |
|
||||
| ------------ | --------- |
|
||||
| `rgtrUuid` | |
|
||||
| `crtCmptnDt` | |
|
||||
| `file` | multipart |
|
||||
|
||||
### 6-2. `PwrkCnsltnMng*`
|
||||
|
||||
**Search**
|
||||
|
||||
`pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm`
|
||||
|
||||
**Save Request (초안 생성)** — 필드가 얇으므로 interface만으로도 가능. 권장은 zod.
|
||||
|
||||
| 필드 | 필수 | 값 |
|
||||
| ----------------- | ---- | ----------------- |
|
||||
| `upDocNo` | Y | 재원협의 문서번호 |
|
||||
| `upDocSeComCd` | | `"FNRS"` |
|
||||
| `prgrsSttsComCd` | | `"DV"` (초안생성) |
|
||||
| `pwrkCnsltnDocNo` | N | 서버 발급 |
|
||||
|
||||
**Save Response** — `save`만 이 DTO. `fnrsCnsltnMngService.save`가 `string`을 반환하는 것과 **다름**.
|
||||
|
||||
| 필드 | 의미 |
|
||||
| ------------------ | --------------------------------- |
|
||||
| `pwrkCnsltnDocNo` | 발급/재사용 번호 |
|
||||
| `resultCode` | `ACCEPTED` / `SUCCESS` / 실패코드 |
|
||||
| `crtDmndUuid` | 폴링 키 |
|
||||
| `crtSttsComCd` | `R` 생성요청 |
|
||||
| `estimatedMinutes` | 기본 10 |
|
||||
|
||||
**DetResponse 중첩**
|
||||
|
||||
- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …)
|
||||
- `pjtcoAjmtList`: 사업비조정
|
||||
- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과
|
||||
- `splmntDmndList`: 보완요청
|
||||
- `sttsChgHstryList`: 상태변경이력
|
||||
- `trsmFileList`: 전송파일 (데이터 설명 기준. Det DTO Java 필드 확인 후 타입 반영)
|
||||
|
||||
### 6-3. DC RAG
|
||||
|
||||
```ts
|
||||
interface DcRagDocumentTransferRequestDto {
|
||||
fnrsCnsltnDocNo: string;
|
||||
taskSeComCd?: string;
|
||||
taskSeCdNm?: string;
|
||||
}
|
||||
|
||||
interface DcRagCommonResponseDto {
|
||||
resultCode: string;
|
||||
message?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
interface DcRagDraftStatusResponseDto {
|
||||
crtDmndUuid: string;
|
||||
status: DraftStatus;
|
||||
statusName?: string;
|
||||
progressPercent?: number;
|
||||
events?: EventInfoDto[];
|
||||
startedDt?: string;
|
||||
estimatedCompletionDt?: string;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
type DraftStatus =
|
||||
| "REQUESTED"
|
||||
| "DOWNLOADING"
|
||||
| "ANALYZING"
|
||||
| "GENERATING"
|
||||
| "COMPLETED"
|
||||
| "FAILED";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 공통코드
|
||||
|
||||
| 상위코드 | 필드 | 값 | 명칭 |
|
||||
| ----------------------- | ---------------- | --------------- | ------------- |
|
||||
| `ASDOCSE` | `upDocSeComCd` | `FNRS` | 재원협의 |
|
||||
| `ASDOCSE` | 문서구분 | `PWRK` | 실무협의 |
|
||||
| `PWRKSTTS` | `prgrsSttsComCd` | `DV` | 초안생성 |
|
||||
| `PWRKSTTS` | | `AR` | 검토자지정 |
|
||||
| `PWRKSTTS` | | `SV` | 작성중 |
|
||||
| `PWRKSTTS` | | `RB` | 검토회신 |
|
||||
| `PWRKSTTS` | | `RC` | 협의회신 |
|
||||
| `PWRKSTTS` | | `TR` | 검토결과 회신 |
|
||||
| `PWRKSTTS` | | `SL` | 보완요청 |
|
||||
| `PWRKSTTS` | | `AA` | 금액조정중 |
|
||||
| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` | 생성요청 |
|
||||
| | | `I` | 생성중 |
|
||||
| | | `C` | 생성완료 |
|
||||
| | | `F` | 생성실패 |
|
||||
| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` | |
|
||||
|
||||
`CrtDmndResponseDto`는 `@CodeName(upCd = "CRTSTTS")`, 생성요청 단건 DTO는 `AICRTSTTS`를 쓴다. 코드값이 `R/I/C/F`로 같으므로 UI는 코드값으로 분기하고, 표시명은 응답의 `*Nm` 필드를 쓴다.
|
||||
|
||||
초안 생성 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 상세의 `crtSttsComCd = C`.
|
||||
|
||||
---
|
||||
|
||||
## 8. 프론트엔드 구현 가이드
|
||||
|
||||
### 8-1. 기술 스택·원칙
|
||||
|
||||
- App Router + FSD (`app/` → `pages/` → `widgets/` → `entities/` → `shared/`)
|
||||
- React Query. ad-hoc `useQuery` + service 직접 호출 금지
|
||||
- `@common/components`, `useDataTable`, `@tanstack/react-table`
|
||||
- `'use client'`는 Web API(다운로드, 폴링, 웹기안기)가 있는 페이지만
|
||||
- 함수명 = Controller public 메서드명 (`delete` → `remove`)
|
||||
- 타입명 = 백엔드 DTO명. 프론트 전용 축약 금지
|
||||
- Save/Apply Request = zod SSOT. Response는 `interface`
|
||||
|
||||
참조 슬라이스: `entities/fnrs-cnsltn/`, `entities/sewer-bsc-plan/`
|
||||
|
||||
### 8-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| -------------------- | ------------------------------------------------------------------- |
|
||||
| ENDPOINT | `apps/as/src/shared/config/api.ts` |
|
||||
| 실무협의 관리 엔티티 | `entities/pwrk-cnsltn-mng/` |
|
||||
| 생성결과 엔티티 | `entities/pwrk-cnsltn-doc-crt-rslt/` |
|
||||
| DC RAG 엔티티 | `entities/dcrag-to/` |
|
||||
| 목록 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/page.tsx` |
|
||||
| 상세 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/[id]/page.tsx` |
|
||||
| 페이지 UI | `pages/pwrk-cnsltn/` |
|
||||
| 재원협의 상세 연계 | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` (버튼·폴링·첨부 검증배지) |
|
||||
|
||||
엔티티 내부:
|
||||
|
||||
```
|
||||
entities/pwrk-cnsltn-mng/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-mng-interface.ts
|
||||
│ └── pwrk-cnsltn-mng-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-mng-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
|
||||
entities/pwrk-cnsltn-doc-crt-rslt/
|
||||
├── model/
|
||||
│ ├── pwrk-cnsltn-doc-crt-rslt-interface.ts
|
||||
│ └── pwrk-cnsltn-doc-crt-rslt-payload-schema.ts
|
||||
├── api/
|
||||
│ ├── pwrk-cnsltn-doc-crt-rslt-service.ts
|
||||
│ └── queries.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### 8-3. Service 시그니처 요지
|
||||
|
||||
`pwrkCnsltnMngService.save` 반환 타입이 `string`이 아니라 `PwrkCnsltnMngSaveResponseDto` 인 점을 `fnrs-cnsltn`과 혼동하지 말 것.
|
||||
|
||||
```ts
|
||||
export interface PwrkCnsltnMngService {
|
||||
findPage(searchDto: PwrkCnsltnMngSearchDto): Promise<PageResponse<PwrkCnsltnMngResponseDto>>;
|
||||
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
|
||||
save(body: PwrkCnsltnMngSaveRequestDto): Promise<PwrkCnsltnMngSaveResponseDto>;
|
||||
saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise<string>;
|
||||
submitSupplement(pwrkCnsltnDocNo: string, body: PwrkCnsltnApplySaveRequestDto): Promise<string>;
|
||||
submitReqSupplement(
|
||||
pwrkCnsltnDocNo: string,
|
||||
body: PwrkCnsltnApplySaveRequestDto,
|
||||
): Promise<string>;
|
||||
remove(pwrkCnsltnDocNo: string): Promise<void>;
|
||||
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PwrkCnsltnDocCrtRsltService {
|
||||
findPage(
|
||||
searchDto: PwrkCnsltnDocCrtRsltSearchDto,
|
||||
): Promise<PageResponse<PwrkCnsltnDocCrtRsltResponseDto>>;
|
||||
findById(crtDmndUuid: string, verSn: number): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
findLatestByCrtDmndUuid(crtDmndUuid: string): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
download(crtDmndUuid: string, verSn: number): Promise<AxiosResponse<Blob>>;
|
||||
downloadLatest(crtDmndUuid: string): Promise<AxiosResponse<Blob>>;
|
||||
saveMultipart(formData: FormData): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
updateMultipart(
|
||||
crtDmndUuid: string,
|
||||
verSn: number,
|
||||
formData: FormData,
|
||||
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
|
||||
remove(crtDmndUuid: string, verSn: number): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
multipart `Content-Type`은 axios가 boundary를 붙이도록 **헤더를 수동 지정하지 않는다.**
|
||||
|
||||
### 8-4. 폴링 훅
|
||||
|
||||
`useQuery` + `refetchInterval`:
|
||||
|
||||
```ts
|
||||
useQuery({
|
||||
queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)],
|
||||
queryFn: () => dcragToService.getDraftGenerationStatus(crtDmndUuid),
|
||||
enabled: Boolean(crtDmndUuid) && isPolling,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") return false;
|
||||
return 5_000;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`COMPLETED` 시 `pwrkCnsltnMng` 상세 쿼리를 invalidate 하여 `crtDmndList[].crtRslt`를 갱신한다.
|
||||
|
||||
### 8-5. 재원협의 상세에 붙일 UI
|
||||
|
||||
`pages/fnrs-cnsltn` 조회 모드:
|
||||
|
||||
1. **자료 검증 요청** — `requestDocumentTransfer` → 상세 refetch → 첨부 행에 PASS/FAIL 배지 (`vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`).
|
||||
2. **초안 생성** — 검증이 끝난 뒤 활성. `pwrkCnsltnMngService.save`.
|
||||
3. **진행 표시** — `progressPercent` + 이벤트 타임라인. `estimatedMinutes`(10)를 안내.
|
||||
4. **웹기안기 열기 / 다운로드** — `COMPLETED` 이후에만.
|
||||
|
||||
`findByUpDocNo(fnrsCnsltnDocNo)`로 이미 초안이 있으면 `crtDmndUuid`를 복원해 폴링·열기를 이어간다.
|
||||
|
||||
### 8-6. 실무협의 목록 컬럼 (권장)
|
||||
|
||||
마스터 응답 + 재원협의 조인 필드 기준. 화면설계 확정 전 TBD.
|
||||
|
||||
| 표시명 | Field |
|
||||
| ------------ | ------------------ |
|
||||
| 실무협의번호 | `pwrkCnsltnDocNo` |
|
||||
| 재원협의번호 | `upDocNo` |
|
||||
| 진행상태 | `prgrsSttsComCdNm` |
|
||||
| 사업명 | `bizNm` |
|
||||
| 시도/구군 | `sido` / `gugun` |
|
||||
| 신청자 | `aplcntNm` |
|
||||
| 등록일 | `regDt` |
|
||||
|
||||
행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}`.
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 순서
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] 1. ENDPOINT 3개 추가 (PWRK_CNSLTN_MNG, PWRK_CNSLTN_DOC_CRT_RSLT, DCRAG_TO)
|
||||
- [ ] 2. entities/dcrag-to (transfer + draft status)
|
||||
- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, save)
|
||||
- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (findPage, downloadLatest, saveMultipart)
|
||||
- [ ] 5. 재원협의 상세: 검증결과 3필드 타입 확장 + 배지
|
||||
- [ ] 6. 재원협의 상세: 자료검증 / 초안생성 / 폴링 UI
|
||||
- [ ] 7. 실무협의 목록·상세 라우트
|
||||
- [ ] 8. 다운로드 (blob)
|
||||
- [ ] 9. 웹기안기 Open — 백엔드 webhwp/open 확인 후
|
||||
- [ ] 10. 웹기안기 저장 → saveMultipart (verSn + 1)
|
||||
```
|
||||
|
||||
1~4는 엔티티 레이어만으로 먼저 닫는다. 화면은 5부터.
|
||||
|
||||
---
|
||||
|
||||
## 10. TBD · 백엔드 확인
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `webhwp/open` | 데이터 설명에는 있으나 **현재 Controller에 없음**. 추가 일정·인증 방식 확인 |
|
||||
| Content-Type | Controller `application/x-hwp` vs 설명 `application/haansofthwpx` |
|
||||
| 파일 확장자 | 다운로드 파일명이 `.hwp`로 고정. 실제 바이너리가 hwpx일 수 있음 |
|
||||
| 폴링 주기 | 5초 vs 10초, 탭 비활성 시 중단 여부 |
|
||||
| 검증 필드 타입 | `FnrsCnsltnMngDetResponseDto.esntlFileList` / `etcFileList` Java 타입이 아직 `FnrsCnsltnAtflResponseDto`가 아님. 런타임 JSON에 `vrfc*`가 오는지 확인 |
|
||||
| `trsmFileList` | 데이터 설명 상세 예시에는 있으나 Det DTO Java 선언과 불일치 가능 |
|
||||
| `crtSttsComCd` 상위코드 | `CRTSTTS` vs `AICRTSTTS` |
|
||||
| 웹기안기 라이선스 URL | `HwpCtrl.Open`에 넘길 게이트웨이 절대 경로 (쿠키 인증 가능 여부) |
|
||||
| 초안 생성 1회 제한 | `requestDraftGeneration` 주석에 “1회 제한”. 재클릭 UX (비활성 / 재요청 확인) |
|
||||
| 자료전송 허용 확장자 | `.doc .docx .hwp .hwpx .md .pdf .txt .xls .xlsm .xlsx`. 이미지 첨부 시 DC RAG 실패 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 참고
|
||||
|
||||
| 자료 | 위치 |
|
||||
| ---------------------- | ------------------------------------------------------------------------------ |
|
||||
| 생성결과 컨트롤러 | `backend/.../pwrkcnsltndoccrtrslt/PwrkCnsltnDocCrtRsltController.java` |
|
||||
| 실무협의관리 컨트롤러 | `backend/.../pwrkcnsltnmng/PwrkCnsltnMngController.java` |
|
||||
| DC RAG TO 컨트롤러 | `backend/.../dcrag/DcRagApiToController.java` |
|
||||
| 초안생성 데이터 설명 | `02.03.초안생성_데이터_설명.md` |
|
||||
| 엔티티 구현 스킬 | `frontend/.cursor/skills/backend-controller-to-entity/` |
|
||||
| 재원협의 엔티티 (패턴) | `apps/as/src/entities/fnrs-cnsltn/` |
|
||||
| IIPBS 단어목록 | `frontend/docs/IIPBS_단어목록.md` |
|
||||
| 진행상태 코드 | `backend/IIPBS_CS/docs/2026/05.행정지원/01.문서상태별_사용자_상태변경_이력.md` |
|
||||
@@ -1,674 +0,0 @@
|
||||
# dmbpRegist.jsp → React 마이그레이션 분석·구현 가이드
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| 원본 JSP | `adssNewOld/.../fnrsc/dmbp/dmbpRegist.jsp` |
|
||||
| 대상 앱 | `frontend/apps/as` |
|
||||
| 도메인 | 하수도기본계획 (구: 기본계획검토 / dmbp) |
|
||||
| API 엔티티 | `@/entities/sewer-bsc-plan` (`SewerBscPlanController`) |
|
||||
| 참조 패턴 | `@/pages/fnrs-cnsltn` + `@/widgets/pbadms-sprt` |
|
||||
| 문서 목적 | contents 영역 **섹션별** UI·상태·API 매핑 및 구현 방향 |
|
||||
| 작성 기준일 | 2026-07-27 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 마이그레이션 범위
|
||||
|
||||
### 1-1. 포함 (contents만)
|
||||
|
||||
JSP `568~1274`행: `<div class="contents">` ~ `<!-- //contents -->`
|
||||
|
||||
| 영역 | JSP 식별 | 개요 |
|
||||
| ---- | ---------------------------------- | --------------------------------------------------------------- |
|
||||
| A | `gTitle1` | 페이지 타이틀·브레드크럼 |
|
||||
| B | `slide_10` | 기본계획변경 신청(마스터·연도·개요) |
|
||||
| C | `slide_15` | 신청자·설계자 정보 |
|
||||
| D | `slide_19` | 개인정보 동의 |
|
||||
| E | `slide_11` | 사업개요(단계별 인구·보급률) |
|
||||
| F | `slide_12` | 총사업비 — `BizCstMatrixFormSection` 전용 |
|
||||
| G | `slide_13` | 재원분담 |
|
||||
| H | `slide_14` | 처리시설별 하수도정비기본계획 (Checkbox + Tabs) |
|
||||
| I | `slide_16` | 체크리스트 + 필수첨부 + 기타첨부 |
|
||||
| J | JSP 상단 `saveBtn` / `presentnBtn` | React: 폼 하단 액션바 (`fnrs-form-page` 279~323과 동일 구성, API만 sewer) |
|
||||
|
||||
### 1-2. 제외
|
||||
|
||||
- `<%@ include %>`: top / sideMenu / footer / `bscPlnPopup.jsp` / `sndrdBzAmnt.jsp`
|
||||
- 레이아웃·메뉴·권한 셸 → 기존 `BaseLayout` / `PageLayout` / AUTH 체계
|
||||
- `mStep1` 진행 단계 표시 (미구현 · 마이그레이션 범위 밖)
|
||||
|
||||
팝업(`이전기본계획조회`)은 **별도 Dialog 컴포넌트**로 재구현하며, include JSP 자체는 포팅하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 목표 아키텍처
|
||||
|
||||
```
|
||||
app/(with-base-layout)/sewer-bsc-plan/regist/page.tsx
|
||||
└─ pages/sewer-bsc-plan/ui/sewer-bsc-plan-regist-page.tsx
|
||||
└─ sewer-bsc-plan-form-page.tsx # FormProvider + 섹션 조립
|
||||
├─ sections/*-form-section.tsx # 도메인 전용
|
||||
└─ widgets/pbadms-sprt/* # 공통 재사용
|
||||
|
||||
entities/sewer-bsc-plan/ # 이미 구현됨
|
||||
saveApply / findDetails / submitSupplement …
|
||||
```
|
||||
|
||||
**폼 스택 (fnrs-cnsltn과 동일)**
|
||||
|
||||
- `react-hook-form` + `zod` (`zodResolver`, `mode: "onChange"`)
|
||||
- 섹션 래퍼: `SectionCollapsible` (Accordion 아님)
|
||||
- UI: `@common/components` (Input, Select, Textarea, Button, Radio, Tabs, Table 등)
|
||||
- 그리드가 필요한 곳만 `@tanstack/react-table` + `DataTable`
|
||||
- 데이터: React Query (`useSaveApplySewerBscPlan` 등) — 목데이터는 API 미연동 구간만 `@faker-js/faker`
|
||||
|
||||
**Vercel React Best Practices (적용 요약)**
|
||||
|
||||
- `client-swr-dedup`: React Query로 체크리스트/필수첨부/코드 조회 중복 제거
|
||||
- `bundle-dynamic-imports`: 처리시설 Tabs 패널·이전계획 Dialog는 `next/dynamic` 후보
|
||||
- `rerender-derived-state`: 단계 연도·합계·진행률은 render 중 파생, 불필요 effect 금지
|
||||
- `async-parallel`: 등록 진입 시 코드/체크리스트트리/필수첨부트리를 병렬 조회
|
||||
|
||||
---
|
||||
|
||||
## 3. 전체 레이아웃
|
||||
|
||||
`fnrs-form-page.tsx`와 동일한 **FormProvider + 세로 섹션 스택** 구조를 따르고,
|
||||
하단 액션바는 **[`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx)** 과 동일 구성(목록·저장·신청완료·삭제·보완완료)이며 **API/라우트만 sewer** 로 바꾼다.
|
||||
타이틀·브레드크럼은 폼 밖(`PageLayout` / regist·edit·view 셸)에서 처리한다.
|
||||
(상세 View의 sticky FAB·검토/결재 버튼은 본 등록 문서 범위 밖 — `fnrs-view-page` 대응 후속.)
|
||||
|
||||
### 3-1. 컴포넌트 트리 (fnrs 대응)
|
||||
|
||||
```
|
||||
PageLayout (title / breadcrumb)
|
||||
└─ SewerBscPlanRegistPage | SewerBscPlanEditPage | SewerBscPlanViewPage
|
||||
└─ SewerBscPlanFormPage | SewerBscPlanViewPage 본문
|
||||
FormProvider (등록/수정) / 읽기 섹션 (상세)
|
||||
└─ <form|div className="mt-6 space-y-4">
|
||||
├─ BasicInfoFormSection // JSP slide_10
|
||||
├─ ContactInfoFormSection // JSP slide_15 (pbadms-sprt)
|
||||
├─ PrivacyConsentSection // JSP slide_19 (pbadms-sprt, 등록·수정)
|
||||
├─ OutlineFormSection // JSP slide_11 (사업개요, 신규)
|
||||
├─ BizCstMatrixFormSection // JSP slide_12 (총사업비 전용, 신규)
|
||||
├─ FnrsCstFormSection // JSP slide_13 (재원분담, 신규 ※ FnrsShar 아님)
|
||||
├─ TmplntFormSection // JSP slide_14 (처리시설, Tabs로 시설별 패널)
|
||||
├─ ChecklistFormSection // JSP slide_16 (pbadms-sprt)
|
||||
├─ AttachmentsFormSection // JSP 필수·기타첨부 (pbadms-sprt)
|
||||
├─ SplmntDmndFormSection // edit + SL 상태만 (pbadms-sprt)
|
||||
└─ form footer action bar // fnrs-form-page 279~323 동일
|
||||
// [목록] / [저장] / edit시 [신청완료][삭제] / SL시 [보완완료]
|
||||
```
|
||||
|
||||
**폼 하단 액션바 버튼 구성**
|
||||
기준: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx). UI·분기만 동일하고 훅/라우트는 sewer.
|
||||
|
||||
| 순서 | 버튼 | 조건 | sewer 비고 |
|
||||
| ---- | -------- | ----------------------------------------- | ---------------------------------------------------- |
|
||||
| 1 | 목록 | 항상 | `router.push("/sewer-bsc-plan")` |
|
||||
| 2 | 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` / 임시저장(SV) |
|
||||
| 3 | 신청완료 | `!isSplmntPrgrsStts && isEditMode` | 체크리스트+필수첨부 검증 → `saveApply` / LE |
|
||||
| 4 | 삭제 | `!isSplmntPrgrsStts && isEditMode` | confirm → `delete` → 목록 |
|
||||
| 5 | 보완완료 | `isSplmntPrgrsStts` (`prgrsSttsComCd=SL`) | `submitSupplement` (fnrs `onSplmntCmptn` 대응) |
|
||||
|
||||
**fnrs와의 차이 (섹션 치환만, 골격 동일)**
|
||||
|
||||
| fnrs-form-page | sewer-bsc-plan |
|
||||
| ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `BasicInfoFormSection` | 동일 역할 (마스터·연도·개요 필드는 sewer DTO) |
|
||||
| `ContactInfoFormSection` | 동일 (`fieldPrefix="sewerBscPlanDtl"`) |
|
||||
| `PrivacyConsentSection` | 동일 |
|
||||
| `BusinessScaleFormSection` | → `OutlineFormSection` (단계별 인구·보급률) |
|
||||
| `BusinessCostFormSection` | → `BizCstMatrixFormSection` (단계×원가 계층 전용) |
|
||||
| `FnrsSharFormSection` | → `FnrsCstFormSection` (국고·지방비·원인자·기타) |
|
||||
| _(없음)_ | `TmplntFormSection` (체크박스 + Tabs 시설별 매트릭스) |
|
||||
| `ChecklistFormSection` `docSeComCd="FNRS"` | `docSeComCd="SEWER"` (TBD) |
|
||||
| `AttachmentsFormSection` | 동일 패턴 |
|
||||
| `SplmntDmndFormSection` | 동일 (`readOnly={!isSplmntPrgrsStts}`) |
|
||||
| 폼 하단 액션바 (목록·저장·신청완료·삭제·보완완료) | **동일 구성, API·라우트만 sewer** |
|
||||
|
||||
### 3-2. 본문 ASCII (폼 페이지)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PageLayout: 하수도기본계획 신청/수정 breadcrumb │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ FormProvider │
|
||||
│ <form className="mt-6 space-y-4"> │
|
||||
│ │
|
||||
│ ┌─ SectionCollapsible: 기본계획 신청 ─────────────────────┐ │
|
||||
│ │ BasicInfoFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 신청자 정보 ────────────────────────────────────────────┐ │
|
||||
│ │ ContactInfoFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 개인정보 동의 ──────────────────────────────────────────┐ │
|
||||
│ │ PrivacyConsentSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 사업개요 ────────────── [이전기본계획조회] ─────────────┐ │
|
||||
│ │ OutlineFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 총사업비 (백만원) ──────────────────────────────────────┐ │
|
||||
│ │ BizCstMatrixFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 재원분담 (백만원) ──────────────────────────────────────┐ │
|
||||
│ │ FnrsCstFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 처리시설별 하수도정비기본계획 ──────────────────────────┐ │
|
||||
│ │ TmplntFormSection │ │
|
||||
│ │ ├─ BscFctList Checkbox │ │
|
||||
│ │ └─ Tabs (선택 시설별 TabsTrigger / TabsContent) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 체크리스트 ─────────────────────────────────────────────┐ │
|
||||
│ │ ChecklistFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 첨부파일 ───────────────────────────────────────────────┐ │
|
||||
│ │ AttachmentsFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─ 보완요구 (edit + SL) ───────────────────────────────────┐ │
|
||||
│ │ SplmntDmndFormSection │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ form footer (fnrs-form-page 279~323 동일) ──────────────┐ │
|
||||
│ │ [목록] [저장] / edit: [신청완료][삭제] / SL: [보완완료] │ │
|
||||
│ │ ※ API·라우트만 sewer — UI 분기는 fnrs와 동일 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ </form> │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-3. 액션바 동작 (`fnrs-form-page` 279~323 동일, API만 sewer)
|
||||
|
||||
구현 위치: `sewer-bsc-plan-form-page.tsx` 하단
|
||||
(`fnrs-form-page.tsx`의 `mx-4 flex flex-wrap justify-center gap-3` 블록을 그대로 옮기고 훅·path만 교체)
|
||||
|
||||
| 버튼 | 조건 | 대표 동작 |
|
||||
| -------- | ---------------------------- | -------------------------------------------------------------------------- |
|
||||
| 목록 | 항상 | `router.push("/sewer-bsc-plan")` |
|
||||
| 저장 | `!isSplmntPrgrsStts` | `type="submit"` → `saveApply` + 임시저장(`SV`) |
|
||||
| 신청완료 | edit && `!isSplmntPrgrsStts` | 체크리스트 완료 + **필수 첨부 완료** 검증 → `saveApply` + `LE` |
|
||||
| 삭제 | edit && `!isSplmntPrgrsStts` | confirm → `delete` → 목록 |
|
||||
| 보완완료 | `isSplmntPrgrsStts` (SL) | `form.handleSubmit` → `useSubmitSupplementSewerBscPlan` |
|
||||
|
||||
**신청완료 사전 검증** (fnrs `handleApplyClick`과 동일 정책)
|
||||
|
||||
1. **체크리스트**: `isFormChecklistComplete(cklstResultList)` — 미완료 시 `"모든 체크리스트 항목을 체크해야 신청이 가능합니다."`
|
||||
2. **필수 첨부**: `esntlAtchYn === "Y"`인 필수첨부 슬롯마다 `esntlAtchFileList`에 `atchFileGroupId`가 있어야 함
|
||||
- Form: `AttachmentsFormSection`의 트리 + 폼 `esntlAtchFileList`
|
||||
- 미충족 시 예: `"필수 첨부파일을 모두 등록해야 신청이 가능합니다."`
|
||||
- 헬퍼 후보: `isFormEsntlAtchComplete(requiredTree, esntlAtchFileList)` (`pbadms-sprt`에 추가)
|
||||
|
||||
저장에는 위 검증을 적용하지 않는다 (임시저장 허용).
|
||||
|
||||
저장/제출 버튼을 섹션 헤더(`gTitle2 gRt`)에 두지 않는다 — **폼 하단 액션바 단일 진입점** (§4-J).
|
||||
|
||||
### 3-4. 라우트
|
||||
|
||||
| 화면 | path | 셸 → FormPage |
|
||||
| ---- | --------------------------- | ------------------------------------------ |
|
||||
| 등록 | `/sewer-bsc-plan/regist` | `mode="create"` |
|
||||
| 수정 | `/sewer-bsc-plan/[id]/edit` | `mode="edit"` + `useSewerBscPlanDetails` |
|
||||
| 상세 | `/sewer-bsc-plan/[id]` | ViewPage (본 문서 범위 밖, fnrs-view 대응) |
|
||||
|
||||
`docSeComCd` 후보: 백엔드 체크리스트·필수첨부 등록값 확인 후 **`SEWER`** (또는 레거시 `DMBP`) — **TBD, CMS 코드와 맞출 것**.
|
||||
|
||||
---
|
||||
|
||||
## 4. 섹션별 상세 구현
|
||||
|
||||
### 4-0. Hidden / 세션 컨텍스트 (폼 루트)
|
||||
|
||||
JSP hidden → React 폼 기본값·세션 스토어로 흡수.
|
||||
|
||||
| JSP | 의미 | React |
|
||||
| ------------------------------- | --------------- | ------------------------------------------------------- |
|
||||
| `chgSe` / `chgSeSp` / `chgSeCd` | 변경구분 | `sewerBscPlan.sewerBscPlanSeComCd` + 표시용 코드명 |
|
||||
| `progrsSttus` | SAVE / PRESENTN | 저장·제출 시 `prgrsSttsComCd`로 매핑 (임시저장 vs 제출) |
|
||||
| `planSeCd` | 고정 `A` | 필요 시 상수 또는 API 필드 확인 |
|
||||
| `adstDstrCd` / `rgnCd` | 행정구역 | `sewerBscPlan.admdstCd` |
|
||||
| `sidoNm` / `gugunNm` | 표시용 | 읽기 전용 표시 (`detDto.sido`/`gugun`) |
|
||||
|
||||
구현: `getCreateDefaultValues(userInfo)`에서 로그인 사용자·소속 지자체 주입.
|
||||
|
||||
---
|
||||
|
||||
### 4-A. 타이틀·브레드크럼
|
||||
|
||||
| JSP | React |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| `h3` 기본계획검토 | PageLayout title: **하수도기본계획 신청** (IIPBS 명명) |
|
||||
| 설치인가 및 제원협의 > … | 메뉴 메타 기반 breadcrumb |
|
||||
|
||||
별도 섹션 컴포넌트 불필요. 라우트 `page.tsx` / layout metadata로 처리.
|
||||
|
||||
---
|
||||
|
||||
### 4-B. 기본계획변경 신청 (`slide_10`)
|
||||
|
||||
**파일**: `ui/sections/basic-info-form-section.tsx`
|
||||
**래퍼**: `SectionCollapsible` title=`기본계획 신청`
|
||||
**필드 접두사**: `sewerBscPlan.*` + `sewerBscPlanDtl.*`
|
||||
|
||||
| 표시명 | JSP | API 매핑 | UI | 필수 | 비고 |
|
||||
| ---------------- | ----------------- | ------------------------------- | ------------------ | ---- | ---------------------------------- |
|
||||
| 기본계획명 | `drngMtncPlanNm` | `sewerBscPlan.sewerBscPlanNm` | Input | Y | |
|
||||
| 계획년도 | `planYr` | `sewerBscPlanDtl.planYr` | Select (현재~2005) | Y | |
|
||||
| 기준년도 | `crtrYr` | `sewerBscPlanDtl.crtrYr` | Select | Y | |
|
||||
| 1~4단계 목표연도 | `goalYrStep1~4` | `frlvlGoalYr`~`folvlGoalYr` | Select (현재~+30) | Y | 1단계 변경 시 +5/+10/+15 자동 연동 |
|
||||
| 계획개요 | `bzCn` | `sewerBscPlanDtl.bizOtlnCn` | Textarea | Y | |
|
||||
| 구분 | `#chgSeNm` | `sewerBscPlanSeComCd` (+코드명) | 읽기 또는 Select | | 신규/변경 등 |
|
||||
| 신청 지자체 | `${lclgvrnmntNm}` | `admdstCd` | 읽기 전용 | | |
|
||||
|
||||
**동작**
|
||||
|
||||
- `goalYrStep1` onChange → 2·3·4단계 기본값 세팅 + E/F/G 헤더 연도 span 갱신 (`watch` 파생)
|
||||
- JSP 상단 `gRt` 저장/제출은 사용하지 않음 → FormPage 하단 액션바 (§3-3, §4-J)
|
||||
|
||||
---
|
||||
|
||||
### 4-C. 신청자 정보 (`slide_15`)
|
||||
|
||||
**재사용**: `@/widgets/pbadms-sprt`의 `ContactInfoFormSection`
|
||||
**props**: `fieldPrefix="sewerBscPlanDtl"` (또는 dtl 필드 구조에 맞게)
|
||||
|
||||
| 표시 | JSP | API |
|
||||
| ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| 신청자 이름/전화/부서/이메일 | `${UsrNm}` 등 서버렌더 | 로그인 사용자 읽기 전용 → `aplcntNm`, `aplcntMblTelno`, `aplcntOgdpNm`, `aplcntEmlAddr` |
|
||||
| 설계자 | `dsgnrNm/Telno/Psitn/Email` | `dsgnrNm`, `dsgnrTelno`, `dsgnrOgdpNm`, `dsgnrEmlAddr` |
|
||||
|
||||
신청자 4필드는 세션에서 defaultValues에 채우고 Input `disabled`.
|
||||
|
||||
---
|
||||
|
||||
### 4-D. 개인정보 동의 (`slide_19`)
|
||||
|
||||
**재사용**: `PrivacyConsentSection`
|
||||
|
||||
| JSP | React |
|
||||
| ----------------------------- | ------------------------------------------------------------------ |
|
||||
| 고지 textarea + 보유기간 문구 | 동일 문구 상수/마크다운 |
|
||||
| `privacyAgree` Y/N | 폼 필드 `privacyAgree: "Y" \| "N"` (API 미전송, 클라이언트 검증만) |
|
||||
|
||||
**검증**: 제출 시 `Y` 필수 (JSP `presentnBtn`과 동일). 저장(임시)은 동의 없이 허용할지 정책 TBD — 기본은 JSP와 같이 **제출만 강제**.
|
||||
|
||||
---
|
||||
|
||||
### 4-E. 사업개요 (`slide_11`)
|
||||
|
||||
**파일**: `ui/sections/outline-form-section.tsx` (신규, 도메인 전용)
|
||||
**API**: `sewerBscPlanOtlnList: SewerBscPlanOtlnItem[]` (planStpNo 1~4)
|
||||
|
||||
| 행(구분) | JSP name | Item 필드 |
|
||||
| ------------ | --------------- | ------------------- |
|
||||
| 계획인구 | `arrPlanPpltn` | `sewerPlanPpltnCnt` |
|
||||
| 처리인구 | `arrPrcsPpltn` | `sewerPrcsPpltnCnt` |
|
||||
| 하수도보급율 | `arrDrngSplyRt` | `sewerPsplyRt` |
|
||||
|
||||
열: 1~4단계 (헤더에 `frlvlGoalYr` 등 표시).
|
||||
|
||||
**버튼**: `이전기본계획조회` → Dialog (`PreviousPlanLookupDialog`)
|
||||
|
||||
- 레거시: `fnbscPlnPopupToggle` + `bscPlnPopup.jsp`
|
||||
- React: Modal + 조회 API (**TBD**: 이전 계획 조회 엔드포인트; 없으면 1차는 Dialog UI만 + mock)
|
||||
|
||||
숫자 입력: 콤마 제거 후 number 전송 (제출 시 JSP `replaceComma` 대체).
|
||||
|
||||
---
|
||||
|
||||
### 4-F. 총사업비 (`slide_12`)
|
||||
|
||||
**파일**: `ui/sections/biz-cst-matrix-form-section.tsx` (**전용 컴포넌트** — fnrs `BusinessCostFormSection` 재사용 금지)
|
||||
**래퍼**: `SectionCollapsible` title=`총사업비` (단위 `백만원`은 헤더 우측)
|
||||
|
||||
**JSP 현실**: 단계(1~4) × 원가 계층(총사업비, 공사비, 처리시설, 하수관로 신설/보수, 펌프장, 배수설비, 기타, 부대경비, 설계/감리/부대비). 일부 readonly·자동합산 JS.
|
||||
|
||||
**신규 API**: `bizCstList: SewerBscPlanBizCstItem[]` = `{ planStpNo, acntgSeCd, cstrnAmt }`
|
||||
|
||||
**구현 방향 (`BizCstMatrixFormSection` 전용)**
|
||||
|
||||
- 행 = `useAcntgSeMngTree` → `buildAcntgCostItems` (fnrs `BusinessCostFormSection`과 동일 출처)
|
||||
- `costItems` 로드 시 `useEffect`로 `bizCstList`를 `mergeBizCstList(existing, costItems)`로 동기화
|
||||
- 열 = `planStpNo` 1~4 (헤더에 목표연도 표시)
|
||||
- readonly 행(`CostItemDef.readonly=true`)은 children 합산으로 자동계산; Input에 `readOnly` + `bg-muted`
|
||||
- 변경 시 `setAmt` → `recalculateBizCstMatrix(list, costItems)` → `setValue("bizCstList")`
|
||||
- 저장 시 섹션 onChange 합산을 신뢰하여 `bizCstList` 그대로 전송 (`toBizCstSaveList` identity)
|
||||
|
||||
**표준사업비 조회** 버튼은 JSP에서 주석 처리됨 → **미구현**.
|
||||
|
||||
---
|
||||
|
||||
### 4-G. 재원분담 (`slide_13`)
|
||||
|
||||
**주의**: fnrs의 `FnrsSharFormSection`(재원협의 공유구분 트리)과 **다름**.
|
||||
|
||||
| 행 | JSP | `SewerBscPlanFnrsCstItem` |
|
||||
| ------------ | -------------- | ------------------------- |
|
||||
| 총사업비 | `arrTotAmnt` | `sumAmt` (또는 파생) |
|
||||
| 국고 | `arrNlepAmt` | `gvexpAmt` |
|
||||
| 지방비 | `arrLcepAmt` | `lcexpAmt` |
|
||||
| 원인자부담금 | `arrCusrAlotm` | `orgprBdchgAmt` |
|
||||
| 기타 | `arrEtcAmt` | `etcAmt` |
|
||||
|
||||
**파일**: `ui/sections/fnrs-cst-form-section.tsx` (신규)
|
||||
데이터: `fnrsCstList` 길이 4 (`planStpNo` 1~4).
|
||||
|
||||
검증(선택): 단계별 `gvexp+lcexp+orgpr+etc === sumAmt` (JSP `calculateTotalFunding` 참고).
|
||||
|
||||
---
|
||||
|
||||
### 4-H. 처리시설별 하수도정비기본계획 (`slide_14`) — 핵심 복잡도
|
||||
|
||||
**JSP 동작**
|
||||
|
||||
1. `BscFctList` 체크박스로 처리시설 다중 선택 (`arrBscFctCd`)
|
||||
2. 선택 시 `createFormSection(code, name)`로 **당초 4단계 | 현재 | 변경 4단계** 매트릭스 동적 삽입
|
||||
→ **React**: 세로 삽입 대신 `@common/components` `Tabs`로 시설별 패널 관리 (§아래 Tabs 규칙)
|
||||
3. 활성 `formFields`(축소판): 처리구역면적, 하수처리인구, 계획하수량, 시설용량, 분류식 오수/우수, 합류식, 차집, 배수설비, 오수중계/일반/우수 펌프
|
||||
4. 시설별 필수첨부 `createFileAttachmentSection`
|
||||
5. `fetchBasicPlanData`로 당초(이전) 데이터 바인딩
|
||||
|
||||
**API 매핑**
|
||||
|
||||
| UI | DTO |
|
||||
| -------------------- | ------------------------------------------------------------- |
|
||||
| 선택 시설 목록 | `sewerBscPlanTmplntList[].swtrmtFcltCd` (+ `atchFileGroupId`) |
|
||||
| 단계별 수치 (변경측) | `sewerBscPlanTmplntDtlList[]` → `SewerBscPlanTmplntDtlItem` |
|
||||
| 임시저장 | `tmplntTmprStrgList` (편집 중 임시, 필요 시) |
|
||||
|
||||
필드 대응 (변경 단계 기준):
|
||||
|
||||
| UI 라벨 | `SewerBscPlanTmplntDtlItem` |
|
||||
| -------------- | --------------------------- |
|
||||
| 처리구역면적 | `prcsZoneArea` |
|
||||
| 하수처리인구 | `swtrmtPpltnCnt` |
|
||||
| 계획하수량 | `planSwqty` |
|
||||
| 시설용량 | `fcltCpct` |
|
||||
| 분류식 오수 | `sprsysDrtwPplLen` |
|
||||
| 분류식 우수 | `sprsysExlnPplLen` |
|
||||
| 합류식 | `cbsysPplLen` |
|
||||
| 차집관로 | `icptgPplLen` |
|
||||
| 배수설비 | `drgCnt` |
|
||||
| 오수 중계 펌프 | `drtwRlyPumpCnt` |
|
||||
| 오수 일반 펌프 | `drtwPumpCnt` |
|
||||
| 우수 펌프 | `exlnPumpCnt` |
|
||||
|
||||
**구현 구성**
|
||||
|
||||
```
|
||||
ui/sections/tmplnt-form-section.tsx
|
||||
├─ TmplntFacilityPicker # BscFctList Checkbox 목록 (시설 마스터 API TBD)
|
||||
└─ Tabs (@common/components Tabs) # 선택 시설별 동적 패널
|
||||
├─ TabsList / TabsTrigger # value=swtrmtFcltCd, 라벨=시설명
|
||||
└─ TabsContent (시설 1개당)
|
||||
├─ TmplntMatrixTable # 당초|현재|변경 매트릭스 (useFieldArray)
|
||||
└─ TmplntFacilityAttachments # 시설별 필수첨부 (선택)
|
||||
```
|
||||
|
||||
**Tabs 관리 규칙** (`packages/common/components/src/ui/tabs`)
|
||||
|
||||
- `BscFctList` 체크 **선택** → `useFieldArray` append + 해당 시설 `TabsTrigger`/`TabsContent` 추가 → `Tabs` `value`를 신규 시설로 전환
|
||||
- 체크 **해제** → confirm 후 remove + 해당 탭 제거 → 남은 탭 중 하나로 `value` 이동 (없으면 Tabs 영역 비표시)
|
||||
- 선택 시설이 0개면 Tabs UI 숨김; 1개 이상이면 Tabs로 시설별 패널 전환
|
||||
- 매트릭스·첨부는 각 `TabsContent` 안에만 렌더 (JSP의 세로 동적 삽입을 탭 패널로 대체)
|
||||
|
||||
**매트릭스 열**
|
||||
|
||||
- **당초 열**: 이전계획 API 조회 결과 읽기 전용 (없으면 빈 칸)
|
||||
- **현재 열**: 기준년도 시점 값 (API 제공 시)
|
||||
- **변경 열**: 편집 가능 → 저장 payload의 dtl만 전송
|
||||
|
||||
시설 마스터 목록 API: 레거시 `BscFctList` 대체 — **TBD** (`swtrmtFclt` 계열).
|
||||
|
||||
`bundle-dynamic-imports`: `TmplntFormSection`(또는 TabsContent 내부 매트릭스)을 `next/dynamic` 후보.
|
||||
|
||||
---
|
||||
|
||||
### 4-I. 체크리스트 + 첨부 (`slide_16`)
|
||||
|
||||
#### I-1. 체크리스트
|
||||
|
||||
**재사용**: `ChecklistFormSection`
|
||||
`docSeComCd="<SEWER|DMBP TBD>"`
|
||||
`fieldPrefix`: 마스터에서 `taskSeComCd`를 읽는 경로 (예: `sewerBscPlan`)
|
||||
|
||||
| JSP | React |
|
||||
| ----------------------- | ------------------------------------------- |
|
||||
| 업무탭 `ceckEssntlArr` | 코드 `taskSeComCd` 다중 → Tabs |
|
||||
| 진행률 / 전체 예·아니오 | 위젯 내장 로직 |
|
||||
| `exmntResult` Y/N | `cklstResultList[].selected` (+ `picRvwCn`) |
|
||||
|
||||
저장: `cklstResultList: CklstResultItem[]`
|
||||
|
||||
#### I-2. 필수 첨부 (탭별)
|
||||
|
||||
**재사용**: `AttachmentsFormSection`
|
||||
`docSeComCd` 동일, `esntlAtchFileList`
|
||||
|
||||
레거시: 체크리스트 탭마다 `essntlList` + `uploadFile` multipart.
|
||||
신규: TUS/`attach-service` + `esntlAtchFileId` / `atchFileGroupId` (fnrs와 동일).
|
||||
|
||||
#### I-3. 기타 첨부
|
||||
|
||||
`AttachmentsFormSection`의 etc 영역 또는 `atchFileGroupId` on `sewerBscPlanDtl`.
|
||||
JSP `uploadFileETC` multiple → Zip 안내 문구 유지.
|
||||
|
||||
---
|
||||
|
||||
### 4-J. 저장 / 제출
|
||||
|
||||
**UI 구성**: [`fnrs-form-page.tsx` 279~323](../../src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx) 과 **동일**.
|
||||
마크업·분기(`isSplmntPrgrsStts` / `isEditMode`)·버튼 색상·`disabled={isSubmitting}` 패턴을 그대로 두고, **호출 API·목록 path만 sewer** 로 교체한다.
|
||||
|
||||
```tsx
|
||||
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
|
||||
<Button color="white" type="button" onClick={() => router.push("/sewer-bsc-plan")} disabled={isSubmitting}>
|
||||
목록
|
||||
</Button>
|
||||
{!isSplmntPrgrsStts ? (
|
||||
<>
|
||||
<Button type="submit" disabled={isSubmitting}>저장</Button>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Button color="green" type="button" onClick={handleApplyClick} disabled={isSubmitting}>
|
||||
신청완료
|
||||
</Button>
|
||||
<Button color="red" type="button" onClick={handleDelClick} disabled={isSubmitting}>
|
||||
삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button type="button" onClick={form.handleSubmit(onSplmntCmptn)} disabled={isSubmitting}>
|
||||
보완완료
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
| 버튼 | fnrs 대응 | sewer API / 동작 |
|
||||
| -------- | ----------------------------- | ------------------------------------------------------------ |
|
||||
| 목록 | `router.push("/fnrs-cnsltn")` | `router.push("/sewer-bsc-plan")` |
|
||||
| 저장 | `saveApply` + 임시저장 | `useSaveApplySewerBscPlan` + `prgrsSttsComCd`=임시저장(`SV`) |
|
||||
| 신청완료 | `handleApplyClick` → 제출 | 체크리스트+필수첨부 검증 → `saveApply` + `LE` (상태코드 TBD) |
|
||||
| 삭제 | `handleDelClick` | `useDeleteSewerBscPlan`(또는 동등) → 목록 |
|
||||
| 보완완료 | `onSplmntCmptn` | `useSubmitSupplementSewerBscPlan` |
|
||||
|
||||
| JSP | 진행값 | URL | React |
|
||||
| ---- | ---------- | ------------------------- | ---------------------------------------------------------- |
|
||||
| 저장 | `SAVE` | `insertFnrscDmbp.do` | `saveApply` + 임시저장 코드 |
|
||||
| 제출 | `PRESENTN` | `insertFnrscDscss.do` (!) | `saveApply` + 제출 상태코드 — **백엔드 상태코드 확인 TBD** |
|
||||
|
||||
페이로드 조립 (`buildApplyPayload`):
|
||||
|
||||
```ts
|
||||
SewerBscPlanApplySaveRequestDto {
|
||||
sewerBscPlan, sewerBscPlanDtl,
|
||||
sewerBscPlanOtlnList, bizCstList, fnrsCstList,
|
||||
sewerBscPlanTmplntList, tmplntTmprStrgList?,
|
||||
cklstResultList, esntlAtchFileList,
|
||||
splmntDmndList?, rvwOpnnList? // 등록 화면에서는 보통 빈 배열
|
||||
}
|
||||
```
|
||||
|
||||
성공 후: `invalidateQueries(sewerBscPlanBaseKey)` → `router.push('/sewer-bsc-plan')` (신청완료는 상세로 보낼 수 있음 — fnrs와 동일 정책).
|
||||
|
||||
권한: JSP `AUTH3` → 기존 AS 권한 훅/역할로 버튼 `disabled`/`hidden`.
|
||||
|
||||
---
|
||||
|
||||
## 5. API / DTO 연결
|
||||
|
||||
Base: `/as-service/api/v1/sewer-bsc-plans`
|
||||
엔티티: `@/entities/sewer-bsc-plan`
|
||||
|
||||
| 화면 동작 | Hook / Service | 비고 |
|
||||
| -------------- | ---------------------------------------- | ---------------- |
|
||||
| 등록 저장·제출 | `useSaveApplySewerBscPlan` → `saveApply` | 본 문서 핵심 |
|
||||
| 수정 로드 | `useSewerBscPlanDetails(docNo, chgCycl)` | 편집 시 |
|
||||
| 마스터만 | `useSewerBscPlan` | 목록 행 클릭 등 |
|
||||
| 보완 제출 | `useSubmitSupplementSewerBscPlan` | edit + `prgrsSttsComCd=SL` 시 폼 하단 [보완완료] |
|
||||
| 기술검토 | `useTechReviewSewerBscPlan` | 등록 JSP 범위 밖 |
|
||||
|
||||
등록 JSP에 없는 섹션(보완요구·검토의견)은 **상세/검토 화면**에서 `SplmntDmndFormSection` / 검토의견 섹션으로 후속 구현.
|
||||
|
||||
---
|
||||
|
||||
## 6. 권장 파일 배치
|
||||
|
||||
```
|
||||
apps/as/
|
||||
├── app/(with-base-layout)/sewer-bsc-plan/
|
||||
│ └── regist/page.tsx
|
||||
├── src/pages/sewer-bsc-plan/
|
||||
│ ├── lib/
|
||||
│ │ ├── sewer-bsc-plan-regist-schema.ts # zod
|
||||
│ │ └── form-data-mapper.ts # Det ↔ Form, Apply payload
|
||||
│ ├── model/
|
||||
│ │ └── (필요 시 table columns)
|
||||
│ └── ui/
|
||||
│ ├── sewer-bsc-plan-regist-page.tsx
|
||||
│ ├── sewer-bsc-plan-form-page.tsx
|
||||
│ ├── components/
|
||||
│ │ └── previous-plan-lookup-dialog.tsx
|
||||
│ └── sections/
|
||||
│ ├── basic-info-form-section.tsx
|
||||
│ ├── outline-form-section.tsx # 사업개요
|
||||
│ ├── biz-cst-matrix-form-section.tsx # 총사업비 (전용)
|
||||
│ ├── fnrs-cst-form-section.tsx # 재원분담
|
||||
│ └── tmplnt-form-section.tsx # 처리시설 (Checkbox + Tabs)
|
||||
└── docs/sewer-bsc-plan/
|
||||
└── dmbp-regist-migration.md # 본 문서
|
||||
```
|
||||
|
||||
공통 재사용 (신규 작성 최소화):
|
||||
|
||||
- `ContactInfoFormSection`, `PrivacyConsentSection`
|
||||
- `ChecklistFormSection`, `AttachmentsFormSection`
|
||||
- `SectionCollapsible`
|
||||
- `Tabs` / `TabsList` / `TabsTrigger` / `TabsContent` (`@common/components` — 처리시설 패널)
|
||||
|
||||
도메인 전용 (재사용 금지):
|
||||
|
||||
- `BizCstMatrixFormSection` — 총사업비 (fnrs `BusinessCostFormSection` 미사용)
|
||||
- 폼 하단 액션바 — `fnrs-form-page` 279~323 복제, API만 sewer
|
||||
|
||||
---
|
||||
|
||||
## 7. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입 (등록)
|
||||
CALL 로그인사용자조회
|
||||
CALL 코드조회(SEWERBSCPLANSE, SEWER, …) // 병렬
|
||||
CALL 체크리스트트리(docSeComCd) // 병렬
|
||||
CALL 필수첨부트리(docSeComCd) // 병렬
|
||||
CALL 처리시설마스터목록 // TBD (BscFctList)
|
||||
DISPLAY 폼 defaultValues
|
||||
|
||||
ON 1단계목표연도 CHANGE DO
|
||||
SET 2·3·4단계 연도 (+5/+10/+15)
|
||||
DISPLAY E/F/G/H 헤더 연도
|
||||
END ON
|
||||
|
||||
ON 처리시설 체크 DO
|
||||
ADD tmplntList 항목
|
||||
ADD TabsTrigger / TabsContent (시설 패널)
|
||||
SET Tabs value = 신규 시설
|
||||
CALL 이전기본계획수치(시설코드) // 당초열, TBD
|
||||
END ON
|
||||
|
||||
ON 처리시설 체크해제 DO
|
||||
CONFIRM 제거
|
||||
REMOVE tmplntList + 해당 Tabs 패널
|
||||
SET Tabs value = 남은 시설 (없으면 Tabs 숨김)
|
||||
END ON
|
||||
|
||||
ON [이전기본계획조회] CLICK DO
|
||||
OPEN PreviousPlanLookupDialog
|
||||
END ON
|
||||
|
||||
ON [저장] CLICK DO
|
||||
VALIDATE zod (동의 제외 가능)
|
||||
CALL saveApply(payload, 임시저장 상태)
|
||||
NAVIGATE 목록
|
||||
END ON
|
||||
|
||||
ON [신청완료] CLICK DO
|
||||
VALIDATE zod + privacyAgree=Y + 체크리스트 완료 + 필수첨부(esntlAtchYn=Y) 완료
|
||||
CONFIRM 신청 확인
|
||||
CALL saveApply(payload, 제출 상태 LE)
|
||||
NAVIGATE 상세
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. JSP ↔ 신규 갭·리스크
|
||||
|
||||
| # | 이슈 | 대응 |
|
||||
| --- | ---------------------------------------------------- | ------------------------------------------------- |
|
||||
| 1 | JSP 제출 URL이 `dscss`로 분기 (오타/구프로세스 가능) | 신규는 `saveApply` 단일 진입으로 통일 |
|
||||
| 2 | 총사업비 UI 계층 ≠ `BizCstItem` | `BizCstMatrixFormSection` 전용 + `acntgSeCd` 매핑 테이블 |
|
||||
| 3 | 재원분담 ≠ fnrs shar | 전용 `fnrs-cst-form-section` |
|
||||
| 4 | 처리시설 매트릭스(당초/현재/변경) | Checkbox 선택 → Tabs 패널; 저장은 변경 dtl 중심; 당초는 조회 전용 |
|
||||
| 5 | 시설·이전계획 API | TBD — 미정 시 체크박스 목록 mock(`faker`)만 |
|
||||
| 6 | `docSeComCd` 값 | CMS/체크리스트 마스터와 합의 |
|
||||
| 7 | 레거시 multipart vs TUS | fnrs 첨부 패턴 준수 |
|
||||
| 8 | 주석 처리된 상세 `formFields` (자연발생인구 등) | **축소된 formFields만** 이관; 주석 블록 복원 금지 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 순서 (권장)
|
||||
|
||||
1. 라우트 + `FormPage` 골격 + zod 스키마(마스터·dtl·동의) + 폼 하단 액션바(§4-J)
|
||||
2. B·C·D 섹션 (공통 위젯 최대 활용)
|
||||
3. E 사업개요 + G 재원분담 (4열 매트릭스 패턴 공유)
|
||||
4. F 총사업비 (`BizCstMatrixFormSection` 전용)
|
||||
5. I 체크리스트·첨부
|
||||
6. H 처리시설 (`BscFctList` Checkbox + `@common/components` Tabs — 가장 복잡)
|
||||
7. 저장/신청완료/삭제/보완완료 API 연동 (§4-J)
|
||||
8. 이전계획 Dialog (API 확정 후)
|
||||
|
||||
---
|
||||
|
||||
## 10. 체크리스트 (jsp-migration-as)
|
||||
|
||||
- [ ] contents만 이관 (include 제외)
|
||||
- [ ] vercel-react-best-practices 적용 (Query 병렬·파생상태·dynamic)
|
||||
- [ ] `@common/components` + 필요 시 DataTable
|
||||
- [ ] API·모델은 `@/entities/sewer-bsc-plan` 우선
|
||||
- [ ] 목데이터는 `@faker-js/faker`만 (시설 마스터 등 TBD 구간)
|
||||
- [ ] 로딩·빈·에러 상태
|
||||
- [ ] `router.push`로 목록 복귀
|
||||
|
||||
---
|
||||
|
||||
## 11. 후속 문서
|
||||
|
||||
- 목록(`dmbpList.jsp`) / 상세(`dmbpView.jsp`) / 수정(`dmbpModify.jsp`)은 본 문서와 동일 섹션 체계로 별도 확장
|
||||
- 화면설계서 SPEC 형식이 필요하면 `docs/sewer-bsc-plan/SPEC.md`로 분리 (영역 A~J 표 + API 표)
|
||||
@@ -1,407 +0,0 @@
|
||||
# 화면설계서: 결재선 관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 0.3 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 결재선 관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 시스템관리 > 결재선 관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
전자결재 처리에 사용할 **결재서식**(템플릿)을 등록·관리하고, 각 서식별로 **결재선**(결재구분·결재자·순서)을 구성한다. 상단의 결재서식 목록에서 하나를 선택하면 하단 영역에 해당 서식의 결재선이 표시되며, 사용자는 **그리드 인라인 폼**으로 결재자를 등록·수정한다. **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다**(단일 행 편집 잠금). 드래그로 순서를 변경한 뒤 **[결재선 저장]** 으로 서버에 반영한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 결재선 관리 브레드크럼: 디지털행정지원 > … > │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 전자결재선 관리 목록 [서식 등록] │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 전자결재서식아이디 | 결재서식명 | 문서구분 | 등록일시 | 관리 │ │
|
||||
│ ├──────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ TMPL-0001 (선택 행 강조) … [수정][삭제] │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
├───────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 결재선 관리 선택된 서식: TMPL-0001 [결재선 저장] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ [영역 B-2] 결재선 그리드 (인라인 편집 + 행 드래그 — @dnd-kit) │ │
|
||||
│ │ ⋮⋮ | 결재선아이디 | 결재순서 | 결재구분 ▼ | 결재자 [선택] | 등록일시 | 관리 │ │
|
||||
│ │ ⋮⋮ | APLN-0001 | [ 1 ] | [검토자 ▼] | [김민수 ][선택] | … | [저장][삭제] │ │
|
||||
│ │ ⋮⋮ | (신규) | [ 2 ] | [승인자 ▼] | [ ][선택] | — | [저장][취소] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ [행 추가] │
|
||||
│ ※ 결재자 등록·수정은 그리드 셀에서 직접 입력. 검토인력만 [선택] 팝업/다이얼로그 │
|
||||
│ ※ 행 추가·수정 중: 해당 행만 편집 가능, 나머지 행·[행 추가]·드래그 비활성 │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
CALL GET /as-elapr-service/api/v1/elapr-tmplt
|
||||
DISPLAY 결재서식 그리드 (영역 A)
|
||||
DISPLAY 빈 상태 안내 (영역 B) // 서식 미선택 시 그리드·툴바 비활성
|
||||
|
||||
// 영역 A: 서식 행 선택
|
||||
ON 영역 A 행 CLICK DO
|
||||
SET 선택된 서식 = 행.elaprTmpltId
|
||||
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
|
||||
DISPLAY 결재선 그리드 (영역 B-2) — 조회 모드
|
||||
END ON
|
||||
|
||||
// 영역 A: 신규 서식 등록
|
||||
ON [서식 등록] 버튼 CLICK DO
|
||||
OPEN 결재서식 등록 다이얼로그
|
||||
ON 확인 DO
|
||||
CALL POST /as-elapr-service/api/v1/elapr-tmplt
|
||||
CALL GET /as-elapr-service/api/v1/elapr-tmplt
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 A: 서식 수정·삭제 (다이얼로그 유지)
|
||||
ON 영역 A [수정] / [삭제] DO
|
||||
(기존과 동일 — PUT / DELETE elapr-tmplt)
|
||||
END ON
|
||||
|
||||
// 영역 B: 행 추가 (인라인 신규 행)
|
||||
ON [행 추가] 버튼 CLICK DO
|
||||
VALIDATE 서식 선택됨
|
||||
VALIDATE 편집 중인 행 없음 (editingRowKey == null)
|
||||
ADD 영역 B-2 그리드 맨 아래 편집 가능 행
|
||||
SET editingRowKey = rowKey (draft-{timestamp})
|
||||
SET 편집상태 = 신규
|
||||
SET aplnId = 미설정 (POST 저장 시 서버 fn_gen_apln_id 발급)
|
||||
SET atrzSeqNo = 현재 행 수 + 1
|
||||
SET atrzSeComCd, aprvrUuid = 빈 값
|
||||
DISABLE 나머지 행 편집 · [행 추가] · 드래그 · [결재선 저장](정책에 따라)
|
||||
END ON
|
||||
|
||||
// 영역 B-2: 기존 행 수정 진입
|
||||
ON 행별 [수정] CLICK DO
|
||||
VALIDATE 편집 중인 행 없음
|
||||
SET editingRowId = 행.aplnId
|
||||
SET 해당 행만 편집 모드
|
||||
DISABLE 나머지 행 편집 · [행 추가] · 드래그
|
||||
END ON
|
||||
|
||||
// 영역 B-2: 인라인 편집
|
||||
ON 그리드 셀 변경 DO
|
||||
VALIDATE 변경 대상 행 == editingRowId
|
||||
SET 해당 행 dirty = true
|
||||
END ON
|
||||
|
||||
ON 셀 [선택] (검토인력) CLICK DO
|
||||
OPEN 검토인력 선택 다이얼로그 (단일 선택)
|
||||
ON 선택 DO
|
||||
SET 행.aprvrUuid = 선택인력.uuid
|
||||
SET 행.aprvrNm = 선택인력.성명 // UI 표시용 (응답 DTO에 없음 — 로컬 보조 필드)
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B-2: 행별 저장
|
||||
ON 행별 [저장] CLICK DO
|
||||
VALIDATE atrzSeComCd, aprvrUuid, atrzSeqNo
|
||||
IF 편집상태 = 신규 THEN
|
||||
CALL POST /as-elapr-service/api/v1/elapr-apln
|
||||
ELSE
|
||||
CALL PUT /as-elapr-service/api/v1/elapr-apln/{aplnId}
|
||||
END IF
|
||||
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
|
||||
CLEAR editingRowId
|
||||
ENABLE 전체 행 조회 모드 · [행 추가] · 드래그
|
||||
END ON
|
||||
|
||||
// 영역 B: 결재선 저장 (드래그 순서 일괄 반영)
|
||||
ON [결재선 저장] 버튼 CLICK DO
|
||||
VALIDATE 서식 선택됨
|
||||
VALIDATE 편집 중인 행 없음
|
||||
REORDER atrzSeqNo = 그리드 행 순서 (1..n)
|
||||
FOR EACH dirty 또는 순서 변경된 행 DO
|
||||
CALL PUT /as-elapr-service/api/v1/elapr-apln/{aplnId} // atrzSeqNo 등 반영
|
||||
END FOR
|
||||
FOR EACH 신규 미저장 행 DO
|
||||
CALL POST /as-elapr-service/api/v1/elapr-apln
|
||||
END FOR
|
||||
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
|
||||
TOAST "결재선이 저장되었습니다."
|
||||
END ON
|
||||
|
||||
// 영역 B-2: 행 드래그
|
||||
ON 영역 B-2 행 DRAG END DO
|
||||
VALIDATE 편집 중인 행 없음
|
||||
REORDER 그리드 행
|
||||
SET 각 행 atrzSeqNo = 행 인덱스 + 1 (UI 즉시 반영, 서버는 [결재선 저장] 또는 행별 [저장])
|
||||
END ON
|
||||
|
||||
// 영역 B-2: 행 삭제
|
||||
ON 행별 [삭제] CLICK DO
|
||||
CONFIRM "선택한 결재선을 삭제하시겠습니까?"
|
||||
IF 서버 저장된 행 THEN
|
||||
CALL DELETE /as-elapr-service/api/v1/elapr-apln/{aplnId}
|
||||
END IF
|
||||
REMOVE 그리드 행 (신규 미저장 행은 API 호출 없이 제거)
|
||||
CALL GET /as-elapr-service/api/v1/elapr-apln?elaprTmpltId={id}
|
||||
END ON
|
||||
|
||||
ON 행별 [취소] CLICK DO // 신규·수정 공통
|
||||
IF 편집상태 = 신규 THEN REMOVE 임시 행
|
||||
ELSE RESTORE 행 스냅샷 (수정 취소)
|
||||
CLEAR editingRowId
|
||||
ENABLE 전체 행 조회 모드 · [행 추가] · 드래그
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A: 전자결재선 관리 목록
|
||||
|
||||
선택된 결재서식이 영역 B의 작업 대상이 된다. 행 클릭으로 선택 상태를 표시(좌측 컬러 바·배경 강조)한다.
|
||||
|
||||
### 4-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ----------------------------------------------- |
|
||||
| 섹션 타이틀 | 전자결재선 관리 목록 |
|
||||
| 우측 버튼 | **[서식 등록]** — 결재서식 등록 다이얼로그 오픈 |
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | 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`).
|
||||
|
||||
### 4-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| --------- | -------------- | ----------------------------------------------- |
|
||||
| 서식 등록 | 섹션 우측 상단 | 결재서식 등록 다이얼로그 오픈 |
|
||||
| 수정 | 행별 | 결재서식 수정 다이얼로그 (기존 값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 서식이면 영역 B 초기화. |
|
||||
|
||||
### 4-4. 다이얼로그(개요)
|
||||
|
||||
| 다이얼로그명 | 트리거 | 입력 항목 | 비고 |
|
||||
| ------------------ | ----------- | ----------------------------------------------- | ----------------------------------------- |
|
||||
| 결재서식 등록 | [서식 등록] | `elaprTmpltNm`, `docSeComCd` | `elaprTmpltExpln` 선택, ID는 저장 후 응답 |
|
||||
| 결재서식 수정 | 행별 [수정] | `elaprTmpltNm`, `docSeComCd`, `elaprTmpltExpln` | ID는 변경 불가 |
|
||||
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B: 결재선 관리
|
||||
|
||||
영역 A에서 선택된 `elaprTmpltId` 기준으로 결재선을 관리한다. **등록·수정은 B-2 그리드 인라인 폼**으로 수행하며, 검토인력 지정만 **[선택]** 보조 다이얼로그를 사용한다.
|
||||
|
||||
### 5-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | 결재선 관리 |
|
||||
| 우측 라벨 | `선택된 서식: {elaprTmpltId}` — 미선택 시 영역 B 비활성 |
|
||||
| 우측 버튼 | **[행 추가]** — 편집 중인 행이 없을 때만 활성 · **[결재선 저장]** — 편집 중인 행이 없을 때만 활성(순서·미저장 행 일괄 반영) |
|
||||
|
||||
### 5-2. 영역 B-2: 결재선 그리드 (인라인 편집)
|
||||
|
||||
#### 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 입력 UI (편집 모드) | 비고 |
|
||||
| ------------- | ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| (드래그 핸들) | — | — | `useSortable` 연동 |
|
||||
| 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: `—`, 저장 후 서버 `aplnId` 표시 |
|
||||
| 결재순서번호 | `atrzSeqNo` | 읽기 전용 (그리드 행 순서 1부터 표시) | 저장 시 행 순서 기준 자동 채번 |
|
||||
| 결재구분 | `atrzSeComCd` | Select (`ATRZSECD` 공통코드) | 표시명 `atrzSeComNm` (조회 행) |
|
||||
| 결재자 | `aprvrUuid` | 읽기 전용 이름 + **[선택]** 버튼 | 응답에 성명 없음 — 선택 시 로컬 `aprvrNm` 유지 |
|
||||
| 등록일시 | `regDt` | 읽기 전용 | 신규 행은 `—` |
|
||||
| 관리 | — | 편집 중인 행: **[저장] [취소]** · 조회 행: **[수정] [삭제]** (다른 행 편집 중이면 비활성) |
|
||||
|
||||
#### 인라인 편집 정책
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **단일 행 편집** | **행 추가 또는 한 행 수정 중에는 다른 행을 편집할 수 없다.** `editingRowId` 로 현재 편집 행 1건만 허용. |
|
||||
| 편집 잠금 시 비활성 | 다른 행: 셀·[수정]·[삭제] 비활성 · `[행 추가]` 비활성 · 행 드래그 비활성 · (선택) `[결재선 저장]` 비활성 |
|
||||
| 편집 해제 | 행별 **[저장]** 성공 또는 **[취소]** 시 `editingRowId` 초기화 후 전체 조회 모드 복귀 |
|
||||
| 조회 행 | 편집 중이 아닐 때 읽기 전용. **[수정]** 클릭 시 해당 행만 편집 모드 진입 |
|
||||
| 신규 행 | `[행 추가]` 시 맨 아래 편집 행 1건 생성 (`editingRowId` = 임시 키) |
|
||||
| 필수값 | `atrzSeComCd`, `aprvrUuid` |
|
||||
| 순서 중복 | 저장 시 행 순서로 자동 채번하므로 중복 없음 |
|
||||
| 결재구분 코드 | `ATRZSECD` — 예: `ATRZ01` 검토자, `ATRZ02` 승인자, `ATRZ03` 최종승인자 |
|
||||
| **순서 제약** | `ATRZ01`은 최상단 블록, `ATRZ03`은 최하단 블록. 드래그 종료·저장 시 `validateAtrzOrder`로 검증, 위반 시 적용/저장 불가 (결재구분 변경 시에는 미검증) |
|
||||
| 저장 단위 | 행별 **[저장]** (단건 POST/PUT) 또는 툴바 **[결재선 저장]** (일괄) |
|
||||
|
||||
#### 행 드래그 정렬 (`@dnd-kit` + `useSortable`)
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| 목적 | 결재 단계 순서를 그리드에서 직접 변경 |
|
||||
| 라이브러리 | `@dnd-kit/core`, `@dnd-kit/sortable` — 테이블 행 POC 후 확정 |
|
||||
| 저장 시점 | 드래그는 클라이언트 상태만 변경 → **[결재선 저장]** 또는 행별 **[저장]** 시 `atrzSeqNo` PUT |
|
||||
| 편집 중 | `editingRowKey != null` 이면 드래그 **불가** |
|
||||
| 순서 제약 | 드래그 종료 시 `validateAtrzOrder` — 실패하면 순서 변경 미적용 + 안내. 결재구분 미선택 행은 드래그 불가 |
|
||||
|
||||
### 5-3. 보조 다이얼로그 (검토인력만)
|
||||
|
||||
| 다이얼로그명 | 트리거 | 결과 |
|
||||
| ------------- | ----------------------- | ---------------------------------- |
|
||||
| 검토인력 선택 | 그리드 결재자 셀 [선택] | `aprvrUuid` 및 UI용 `aprvrNm` 반영 |
|
||||
|
||||
> 결재자 **등록·수정 다이얼로그는 사용하지 않음** (v0.2 변경).
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------------------------- |
|
||||
| 서비스 | `as-elapr-service` |
|
||||
| Base prefix | `/as-elapr-service/api/v1` (프록시 경유) |
|
||||
| 인증 | JWT (`Authorization` 헤더) |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 엔티티 | `@/entities/elapr` (`elaprTmpltService`, `elaprAplnService`) |
|
||||
|
||||
### 6-2. 전자결재서식 (영역 A) — `elapr-tmplt`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ----------------------------- | ------------------------------------------ | ------------------ | ---------------------------- |
|
||||
| 목록 조회 | GET | `/elapr-tmplt` | Query: `ElaprTmpltSearchParams` + Pageable | `Page<ElaprTmplt>` | `elaprTmpltService.findPage` |
|
||||
| 단건 조회 | GET | `/elapr-tmplt/{elaprTmpltId}` | — | `ElaprTmplt` | `elaprTmpltService.findById` |
|
||||
| 등록 | POST | `/elapr-tmplt` | `ElaprTmpltSaveRequest` | `ElaprTmplt` (201) | `elaprTmpltService.create` |
|
||||
| 수정 | PUT | `/elapr-tmplt/{elaprTmpltId}` | `ElaprTmpltUpdateRequest` | `ElaprTmplt` | `elaprTmpltService.update` |
|
||||
| 삭제 | DELETE | `/elapr-tmplt/{elaprTmpltId}` | — | 204 | `elaprTmpltService.remove` |
|
||||
|
||||
**ElaprTmplt (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ----------------- | ------- | ------------------ |
|
||||
| `elaprTmpltId` | string | 전자결재서식아이디 |
|
||||
| `elaprTmpltNm` | string | 결재서식명 |
|
||||
| `elaprTmpltExpln` | string? | 서식 설명 |
|
||||
| `docSeComCd` | string? | 문서구분 코드 |
|
||||
| `docSeComNm` | string? | 문서구분명 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**ElaprTmpltSaveRequest**: `elaprTmpltNm`\_, `elaprTmpltExpln?`, `docSeComCd?`
|
||||
|
||||
**ElaprTmpltUpdateRequest**: `elaprTmpltNm`\*, `elaprTmpltExpln?`, `docSeComCd?`
|
||||
|
||||
**ElaprTmpltSearchParams**: `elaprTmpltNm?`, `docSeComCd?`, `regDtStart?`, `regDtEnd?` + `page`, `size`, `sort`
|
||||
|
||||
### 6-3. 결재선 (영역 B) — `elapr-apln`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ---------------------- | ------------------------------- | ----------------- | --------------------------- |
|
||||
| 목록 조회 | GET | `/elapr-apln` | Query: `elaprTmpltId`, Pageable | `Page<ElaprApln>` | `elaprAplnService.findPage` |
|
||||
| 단건 조회 | GET | `/elapr-apln/{aplnId}` | — | `ElaprApln` | `elaprAplnService.findById` |
|
||||
| 등록 | POST | `/elapr-apln` | `ElaprAplnSaveRequest` | `ElaprApln` (201) | `elaprAplnService.create` |
|
||||
| 수정 | PUT | `/elapr-apln/{aplnId}` | `ElaprAplnUpdateRequest` | `ElaprApln` | `elaprAplnService.update` |
|
||||
| 삭제 | DELETE | `/elapr-apln/{aplnId}` | — | 204 | `elaprAplnService.remove` |
|
||||
|
||||
> **일괄 저장 API 없음** — UI의 [결재선 저장]은 변경된 행에 대해 POST/PUT을 순차(또는 병렬) 호출하는 클라이언트 오케스트레이션으로 구현.
|
||||
|
||||
**ElaprApln (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| -------------- | ------- | ------------------ |
|
||||
| `aplnId` | string | 결재선아이디 |
|
||||
| `elaprTmpltId` | string | 전자결재서식아이디 |
|
||||
| `aprvrUuid` | string? | 결재자 UUID |
|
||||
| `atrzSeComCd` | string? | 결재구분 코드 |
|
||||
| `atrzSeComNm` | string? | 결재구분명 |
|
||||
| `atrzSeqNo` | number? | 결재순서번호 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**ElaprAplnSaveRequest**: `elaprTmpltId`\_, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
|
||||
|
||||
**ElaprAplnUpdateRequest**: `elaprTmpltId`\*, `aprvrUuid?`, `atrzSeComCd?`, `atrzSeqNo?`
|
||||
|
||||
### 6-4. 검토인력 (보조)
|
||||
|
||||
| 기능 | Method | URL | 비고 |
|
||||
| ------------------ | ------ | --- | --------------------------- |
|
||||
| 검토인력 목록 조회 | TBD | TBD | 사용자 서비스 또는 별도 API |
|
||||
|
||||
### 6-5. Backend ↔ 화면 필드 매핑 (구 SPEC 대비)
|
||||
|
||||
| 구 SPEC 필드 | 백엔드 필드 |
|
||||
| ----------------- | ---------------------------------------------------- |
|
||||
| `elctAtrzTmpltId` | `elaprTmpltId` |
|
||||
| `atrzTmpltNm` | `elaprTmpltNm` |
|
||||
| `docSeNm` | `docSeComNm` |
|
||||
| `atrzLineId` | `aplnId` |
|
||||
| `atrzSeNm` | `atrzSeComNm` |
|
||||
| `atrzUserNm` | (없음) `aprvrUuid` + 사용자 조회/선택 시 로컬 표시명 |
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- **공통 컴포넌트**: `@common/components`
|
||||
- **테이블**: `@tanstack/react-table` + 인라인 편집 셀 (`Select`, `Input`, `Button`)
|
||||
- **드래그**: `@dnd-kit` — B-2 행 정렬 (POC 필수)
|
||||
- **데이터**: React Query — `@/entities/elapr` hooks
|
||||
- **상태**: 선택된 `elaprTmpltId`, `editingRowId` (단일 행 편집 잠금), B-2 행 배열(dirty·신규·`aprvrNm`·수정 전 스냅샷)
|
||||
- **모의 데이터**: API 연동 전 `@faker-js/faker`
|
||||
|
||||
### 7-2. 모듈·파일 배치(제안)
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------ | -------------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/approval-line-management/` (Public API: page만 export) |
|
||||
| UI | `.../ui/elapr-tmplt-list.tsx`, `elapr-tmplt-form-dialog.tsx`, `elapr-apln-grid.tsx` 등 |
|
||||
| model | `.../model/columns.tsx`, `apln-row-types.ts`, `mock-reviewers.ts` |
|
||||
| lib | `.../lib/use-apln-rows.ts`, `validators.ts`, `use-dnd-sensors.ts` |
|
||||
| 엔티티 | `apps/as/src/entities/elapr/` |
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 주요 UI |
|
||||
| ------ | -------------------------------------------------------------------------------------------- |
|
||||
| A | DataTable + 행 선택 + 서식 등록/수정 다이얼로그 |
|
||||
| B 헤더 | 상태 라벨 + `행 추가`, `결재선 저장` |
|
||||
| B-2 | 인라인 편집 DataTable + `editingRowId` 단일 행 잠금 + 드래그 핸들 + 행별 수정/저장/삭제/취소 |
|
||||
| 공통 | 서식 Dialog, 검토인력 선택 Dialog만 |
|
||||
|
||||
### 7-4. 데이터 흐름(요약)
|
||||
|
||||
- **진입**: `useElaprTmpltPage` → 영역 A. B 비활성.
|
||||
- **서식 선택**: `useElaprAplnPage({ elaprTmpltId })` → B-2.
|
||||
- **행 추가**: `editingRowId` 없을 때만 가능 → 신규 행 1건 편집 잠금 → 저장/취소 후 해제.
|
||||
- **인라인 수정**: [수정]으로 `editingRowId` 설정 → 해당 행만 편집 → `useUpdateElaprApln` 후 해제.
|
||||
- **드래그**: `editingRowId == null` 일 때만 가능 → 로컬 `atrzSeqNo` 재부여 → 일괄 PUT.
|
||||
- **삭제**: `useDeleteElaprApln` 또는 신규 행 로컬 제거.
|
||||
|
||||
### 7-5. 미확정 / 검증 필요
|
||||
|
||||
- 검토인력 API 경로·DTO.
|
||||
- `@dnd-kit` + react-table 행 인라인 Input 동시 적용 POC.
|
||||
- 결재자 성명 표시: `aprvrUuid` → 사용자명 조회 API.
|
||||
- 영역 A·B 페이지네이션·검색 UI 노출 여부.
|
||||
- 권한(역할별 CRUD).
|
||||
@@ -1,487 +0,0 @@
|
||||
# 화면설계서: 체크리스트관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 0.1 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 체크리스트관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 시스템관리 > 체크리스트관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
업무별·문서별 **점검표**(체크리스트)를 등록·관리하고, **점검항목** 마스터를 유지한다. 상단 좌측에서 점검표를 선택하면 하단 **체크리스트 항목** 영역에 해당 점검표의 검토 구성(계층형 검토내용)을 편집한다. 상단 우측 **점검항목 관리**는 점검표에 연결할 항목 풀을 등록·수정·삭제한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 체크리스트관리 브레드크럼: 디지털행정지원 > … > │
|
||||
├──────────────────────────────────────────────┬────────────────────────────────────────────────┤
|
||||
│ [영역 A] 점검표 관리 │ [영역 B] 점검항목 관리 │
|
||||
│ [점검표 추가] │ [점검항목 추가] │
|
||||
│ ┌────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │
|
||||
│ │ 점검표일련번호 | 점검표명 | 문서구분 │ │ │ 점검항목코드 | 점검항목명 | 사용여부 │ │
|
||||
│ │ 업무구분 | 사용여부 | 등록일시 | 관리 │ │ │ 등록일시 | 관리 │ │
|
||||
│ ├────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │
|
||||
│ │ 1 (선택 행 강조) [관리] [삭제] │ │ │ ITEM_001 요건검토 … [관리][삭제] │ │
|
||||
│ │ │ │ │ ITEM_002 법령검토 … [관리][삭제] │ │
|
||||
│ └────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────┴────────────────────────────────────────────────┤
|
||||
│ [영역 C] 체크리스트 항목 [미리보기] [추가] [저장] │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 점검항목 ▼ | 주요 검토내용 | 사용 여부 | 구분 | 관리 │ │
|
||||
│ │ L1 [요건검토 ▼] | (입력) | ○예 ○아니오 | 1단계 | [하위추가]│ │
|
||||
│ │ L2 [요건검토 ▼] | (입력) | ○예 ○아니오 | 2단계 | [삭제] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
CALL GET /as-checklist-service/api/v1/cklst
|
||||
DISPLAY 점검표 그리드 (영역 A)
|
||||
CALL GET /as-checklist-service/api/v1/cklst-artcl
|
||||
DISPLAY 점검항목 그리드 (영역 B)
|
||||
DISPLAY 빈 상태 안내 (영역 C) // 점검표 미선택 시 C 비활성
|
||||
|
||||
// 영역 A: 점검표 행 선택
|
||||
ON 영역 A 행 CLICK DO
|
||||
SET 선택된 점검표 = 행.cklstSn
|
||||
CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn}
|
||||
// TBD: 점검표 단위 전체 검토 트리 조회 API — 현재는 항목별 by-artcl 조합 또는 클라이언트 병합
|
||||
DISPLAY 체크리스트 항목 그리드 (영역 C)
|
||||
END ON
|
||||
|
||||
// 영역 A: 점검표 추가
|
||||
ON [점검표 추가] 버튼 CLICK DO
|
||||
OPEN 점검표 추가 다이얼로그
|
||||
ON [저장] DO
|
||||
VALIDATE cklstNm, taskSeComCd
|
||||
CALL POST /as-checklist-service/api/v1/cklst
|
||||
CALL GET /as-checklist-service/api/v1/cklst
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
ON [취소] / [닫기] DO
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 A: 점검표 수정·삭제
|
||||
ON 영역 A 행 [관리] CLICK DO
|
||||
OPEN 점검표 수정 다이얼로그 (기존 값 초기화)
|
||||
ON [저장] DO
|
||||
CALL PUT /as-checklist-service/api/v1/cklst/{cklstSn}
|
||||
CALL GET /as-checklist-service/api/v1/cklst
|
||||
END ON
|
||||
END ON
|
||||
|
||||
ON 영역 A 행 [삭제] CLICK DO
|
||||
CONFIRM "선택한 점검표를 삭제하시겠습니까?"
|
||||
CALL DELETE /as-checklist-service/api/v1/cklst/{cklstSn}
|
||||
IF 삭제 대상 == 선택된 점검표 THEN RESET 영역 C
|
||||
CALL GET /as-checklist-service/api/v1/cklst
|
||||
END ON
|
||||
|
||||
// 영역 B: 점검항목 추가
|
||||
ON [점검항목 추가] 버튼 CLICK DO
|
||||
OPEN 점검항목 추가 다이얼로그
|
||||
ON [저장] DO
|
||||
VALIDATE cklstArtclNm
|
||||
// TBD: cklstSn 필수 — UI에 소속 점검표 선택 없음. 백엔드 정책 확인
|
||||
CALL POST /as-checklist-service/api/v1/cklst-artcl
|
||||
CALL GET /as-checklist-service/api/v1/cklst-artcl
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B: 점검항목 수정·삭제
|
||||
ON 영역 B 행 [관리] CLICK DO
|
||||
OPEN 점검항목 수정 다이얼로그
|
||||
ON [저장] DO
|
||||
CALL PUT /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn}
|
||||
END ON
|
||||
END ON
|
||||
|
||||
ON 영역 B 행 [삭제] CLICK DO
|
||||
CONFIRM "선택한 점검항목을 삭제하시겠습니까?"
|
||||
CALL DELETE /as-checklist-service/api/v1/cklst-artcl/{cklstArtclSn}/{cklstSn}
|
||||
CALL GET /as-checklist-service/api/v1/cklst-artcl
|
||||
END ON
|
||||
|
||||
// 영역 C: 검토 행 추가
|
||||
ON [추가] 버튼 CLICK DO
|
||||
VALIDATE 선택된 점검표 존재
|
||||
ADD 영역 C 그리드 맨 아래 편집 가능 행 (1단계, rvwCnLvNo=1)
|
||||
END ON
|
||||
|
||||
// 영역 C: 하위 검토 추가
|
||||
ON 행 [하위추가] CLICK DO
|
||||
ADD 부모행 하위 편집 행 (2단계, upCklstRvwSn=부모.cklstRvwSn, rvwCnLvNo=2)
|
||||
END ON
|
||||
|
||||
// 영역 C: 일괄 저장
|
||||
ON [저장] 버튼 CLICK DO
|
||||
VALIDATE 선택된 점검표 존재
|
||||
FOR EACH 신규·변경된 검토 행 DO
|
||||
IF 신규 THEN
|
||||
CALL POST /as-checklist-service/api/v1/cklst-rvw
|
||||
ELSE
|
||||
CALL PUT /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}
|
||||
END IF
|
||||
END FOR
|
||||
CALL GET /as-checklist-service/api/v1/cklst-rvw?cklstSn={cklstSn}
|
||||
TOAST "체크리스트 항목이 저장되었습니다."
|
||||
END ON
|
||||
|
||||
// 영역 C: 행 삭제
|
||||
ON 행 [삭제] CLICK DO
|
||||
CONFIRM "선택한 항목을 삭제하시겠습니까?"
|
||||
IF 서버 저장된 행 THEN
|
||||
CALL DELETE /as-checklist-service/api/v1/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}
|
||||
END IF
|
||||
REMOVE 그리드 행 (미저장 행은 API 호출 없이 제거)
|
||||
END ON
|
||||
|
||||
// 영역 C: 미리보기
|
||||
ON [미리보기] 버튼 CLICK DO
|
||||
OPEN 체크리스트 미리보기 다이얼로그 (읽기 전용 트리)
|
||||
// TBD: 미리보기 UI·API
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A: 점검표 관리
|
||||
|
||||
선택된 점검표가 영역 C의 편집 대상이 된다. 행 클릭으로 선택 상태를 표시한다.
|
||||
|
||||
### 4-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------ |
|
||||
| 섹션 타이틀 | 점검표 관리 |
|
||||
| 우측 버튼 | **[점검표 추가]** — 점검표 등록 다이얼로그 |
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ---------------- | ------------- | ------ | ---------------------------------------------------- |
|
||||
| 점검표일련번호 | `cklstSn` | number | PK |
|
||||
| 점검표명 | `cklstNm` | string | |
|
||||
| 문서구분공통코드 | `docSeComCd` | string | 코드명 표시 — `ASDOCSECD` 등 공통코드 (TBD 상위코드) |
|
||||
| 업무구분공통코드 | `taskSeComCd` | string | RE/SF/SP 등 — 공통코드 (TBD 상위코드) |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
|
||||
- 행 클릭 시: `cklstSn` 선택 → 영역 C 헤더·그리드 갱신.
|
||||
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`).
|
||||
|
||||
### 4-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ----------- | -------------- | ------------------------------------------------- |
|
||||
| 점검표 추가 | 섹션 우측 상단 | 점검표 추가 다이얼로그 오픈 |
|
||||
| 관리 | 행별 | 점검표 수정 다이얼로그 (기존 값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제. 선택 중인 점검표이면 영역 C 초기화. |
|
||||
|
||||
### 4-4. 다이얼로그: 점검표 추가 / 수정
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | ---------------- | ---- | ------ | ----------------- | ------------------------------------------------ |
|
||||
| 점검표명 | Text | Y | — | `cklstNm` | placeholder: "예: 설치인가 체크리스트" |
|
||||
| 문서구분 | Select(공통코드) | N | — | `docSeComCd` | 예: FNRS, INSTL, PWRK, DESIGN |
|
||||
| 업무구분 | Select(공통코드) | Y | — | `taskSeComCd` | 예: RE(재이용수), SF(하수처리시설), SP(하수관로) |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N, UI 표시: 예/아니오 |
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------- |
|
||||
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
|
||||
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
|
||||
| 닫기 | 다이얼로그 닫기 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B: 점검항목 관리
|
||||
|
||||
점검표 하단(영역 C)에서 선택할 **점검항목** 마스터를 관리한다.
|
||||
|
||||
### 5-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ---------------------------------------------- |
|
||||
| 섹션 타이틀 | 점검항목 관리 |
|
||||
| 우측 버튼 | **[점검항목 추가]** — 점검항목 등록 다이얼로그 |
|
||||
|
||||
### 5-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------ | -------------- | ------ | ------------------------------------- |
|
||||
| 점검항목코드 | `cklstArtclSn` | number | UI mock `ITEM_001` 형식 표시 여부 TBD |
|
||||
| 점검항목명 | `cklstArtclNm` | string | |
|
||||
| 사용여부 | `useYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 관리 | — | — | 행별 **[관리] · [삭제]** |
|
||||
|
||||
> **TBD**: 백엔드 `CklstArtcl`은 복합 PK(`cklstArtclSn`, `cklstSn`)이며 등록 시 `cklstSn` 필수. UI 다이얼로그에는 `cklstSn` 입력이 없음 — 소속 점검표 지정 방식·전역 마스터 여부 백엔드 정책 확인.
|
||||
|
||||
### 5-3. 다이얼로그: 점검항목 추가 / 수정
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | --------- | ---- | ------ | ----------------- | --------------------------- |
|
||||
| 점검항목명 | Text | Y | — | `cklstArtclNm` | placeholder: "예: 법령검토" |
|
||||
| 사용여부 | Select | N | Y | `useYn` | Y/N |
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------- |
|
||||
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
|
||||
| 취소 | 다이얼로그 닫기 |
|
||||
| 닫기 | 다이얼로그 닫기 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 영역 C: 체크리스트 항목 (선택된 점검표)
|
||||
|
||||
영역 A에서 선택된 `cklstSn` 기준으로 **점검표검토**(계층형 검토내용)를 구성한다.
|
||||
|
||||
### 6-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | 체크리스트 항목 (선택된 점검표: {cklstNm}) |
|
||||
| 우측 버튼 | **[미리보기]** · **[추가]** · **[저장]** — 점검표 미선택 시 C 비활성 |
|
||||
|
||||
### 6-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 입력 UI | 비고 |
|
||||
| ------------- | -------------- | ------------------------------------- | ------------------------------------ |
|
||||
| 점검항목 | `cklstArtclSn` | Select (영역 B 항목 목록) | L1/L2 접두 표시, `cklstArtclNm` 라벨 |
|
||||
| 주요 검토내용 | `rvwCn` | Text / Textarea | 최대 4000자 |
|
||||
| 사용 여부 | `useYn` | Radio (예/아니오) | `Y` / `N` |
|
||||
| 구분 | `rvwCnLvNo` | 읽기 전용 | 1→1단계, 2→2단계, 3→3단계 |
|
||||
| 관리 | — | **[하위추가]** (1·2단계, 저장 후) · **[삭제]** | 3단계 행은 [하위추가] 미표시 |
|
||||
|
||||
#### 계층 정책
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | -------------------------------------------------------------------- |
|
||||
| 최대 깊이 | 3단계 (`rvwCnLvNo`: 1, 2, 3) |
|
||||
| 상위-하위 연결 | `upCklstRvwSn` — 2단계 행은 부모 1단계 `cklstRvwSn` 참조 |
|
||||
| 정렬 | `sortSeq` — 동일 레벨 내 순서 (TBD: UI 드래그 정렬 여부) |
|
||||
| 체크 사용 | `bchckUseYn` — 미리보기·실사용 화면 체크박스 노출 여부 (TBD UI 반영) |
|
||||
|
||||
### 6-3. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| -------- | ---------------------------------------------------- |
|
||||
| 추가 | 1단계 검토 행 1건 추가 (편집 가능) |
|
||||
| 저장 | 변경·신규 검토 행 일괄 POST/PUT (일괄 저장 API 없음) |
|
||||
| 미리보기 | 선택 점검표 체크리스트 읽기 전용 미리보기 (TBD) |
|
||||
| 하위추가 | 선택 1·2단계 행 아래 하위 단계(2·3단계) 자식 행 추가 |
|
||||
| 삭제 | 확인 후 DELETE 또는 미저장 행 로컬 제거 |
|
||||
|
||||
---
|
||||
|
||||
## 7. API/DTO 명세
|
||||
|
||||
### 7-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| 서비스 | `as-checklist-service` |
|
||||
| Base prefix | `/as-checklist-service/api/v1` (게이트웨이 프록시 경유) |
|
||||
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 엔티티 | `@/entities/cklst` (`cklstMngService`, `cklstArtclService`, `cklstRvwService`) |
|
||||
|
||||
### 7-2. 점검표 (영역 A) — `cklst`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ------------------ | --------------------------------- | ------------------ | -------------------------- |
|
||||
| 목록 조회 | GET | `/cklst` | `CklstMngSearchParams` + Pageable | `Page<CklstMng>` | `cklstMngService.findPage` |
|
||||
| 단건 조회 | GET | `/cklst/{cklstSn}` | — | `CklstMng` | `cklstMngService.findById` |
|
||||
| 등록 | POST | `/cklst` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.create` |
|
||||
| 수정 | PUT | `/cklst/{cklstSn}` | `CklstMngSaveRequest` | `number` (cklstSn) | `cklstMngService.update` |
|
||||
| 삭제 | DELETE | `/cklst/{cklstSn}` | — | `number` (cklstSn) | `cklstMngService.remove` |
|
||||
|
||||
**CklstMng (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------- | ------- | ---------------- |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstNm` | string | 점검표명 |
|
||||
| `taskSeComCd` | string | 업무구분공통코드 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `docSeComCd` | string? | 문서구분공통코드 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**CklstMngSaveRequest**: `cklstNm`\*, `taskSeComCd`\*, `useYn?`, `docSeComCd?`
|
||||
|
||||
**CklstMngSearchParams**: `cklstNm?`, `taskSeComCd?`, `useYn?`, `docSeComCd?` + `page`, `size`, `sort`
|
||||
|
||||
**React Query hooks**: `useCklstMngPage`, `useCklstMng`, `useCreateCklstMng`, `useUpdateCklstMng`, `useDeleteCklstMng`
|
||||
|
||||
### 7-3. 점검표항목 (영역 B) — `cklst-artcl`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| ------------------ | ------ | --------------------------------------- | ----------------------------------- | ------------------ | --------------------------------- |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-artcl` | `CklstArtclSearchParams` + Pageable | `Page<CklstArtcl>` | `cklstArtclService.findPage` |
|
||||
| 점검표별 전체 목록 | GET | `/cklst-artcl/by-cklst/{cklstSn}` | — | `CklstArtcl[]` | `cklstArtclService.findByCklstSn` |
|
||||
| 단건 조회 | GET | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtcl` | `cklstArtclService.findById` |
|
||||
| 등록 | POST | `/cklst-artcl` | `CklstArtclSaveRequest` | `CklstArtclId` | `cklstArtclService.create` |
|
||||
| 수정 | PUT | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | `CklstArtclUpdateRequest` | `CklstArtclId` | `cklstArtclService.update` |
|
||||
| 삭제 | DELETE | `/cklst-artcl/{cklstArtclSn}/{cklstSn}` | — | `CklstArtclId` | `cklstArtclService.remove` |
|
||||
|
||||
**CklstArtcl (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ----------------- | ------- | ------------------ |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `cklstArtclNm` | string? | 점검표항목명 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `rgtr` / `rgtrNm` | string? | 등록자 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**CklstArtclSaveRequest**: `cklstSn`\*, `cklstArtclNm?`, `useYn?`
|
||||
|
||||
**CklstArtclUpdateRequest**: `cklstArtclNm?`, `useYn?`
|
||||
|
||||
**CklstArtclId**: `{ cklstArtclSn: number; cklstSn: number }`
|
||||
|
||||
**React Query hooks**: `useCklstArtclPage`, `useCklstArtclList`, `useCklstArtcl`, `useCreateCklstArtcl`, `useUpdateCklstArtcl`, `useDeleteCklstArtcl`
|
||||
|
||||
### 7-4. 점검표검토 (영역 C) — `cklst-rvw`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------------------- | ------ | -------------------------------------------------- | ------------------------------ | ---------------- | ------------------------------- |
|
||||
| 목록 조회 (페이징) | GET | `/cklst-rvw` | `CklstRvwSearchParams` + Pageable | `Page<CklstRvw>` | `cklstRvwService.findPage` |
|
||||
| 항목별 검토 전체 목록 | GET | `/cklst-rvw/by-artcl/{cklstSn}/{cklstArtclSn}` | — | `CklstRvw[]` | `cklstRvwService.findByArtcl` |
|
||||
| 단건 조회 | GET | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvw` | `cklstRvwService.findById` |
|
||||
| 등록 | POST | `/cklst-rvw` | `CklstRvwSaveRequest` | `CklstRvwId` | `cklstRvwService.create` |
|
||||
| 수정 | PUT | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | `CklstRvwUpdateRequest` | `CklstRvwId` | `cklstRvwService.update` |
|
||||
| 삭제 | DELETE | `/cklst-rvw/{cklstRvwSn}/{cklstArtclSn}/{cklstSn}` | — | `CklstRvwId` | `cklstRvwService.remove` |
|
||||
|
||||
**CklstRvw (응답)**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| -------------- | ------- | ---------------------- |
|
||||
| `cklstRvwSn` | number | 점검표검토일련번호 |
|
||||
| `cklstArtclSn` | number | 점검표항목일련번호 |
|
||||
| `cklstSn` | number | 점검표일련번호 |
|
||||
| `upCklstRvwSn` | number? | 상위점검표검토일련번호 |
|
||||
| `rvwCnLvNo` | number? | 검토내용레벨번호 |
|
||||
| `rvwCn` | string? | 검토내용 |
|
||||
| `bchckUseYn` | string? | 체크사용여부 |
|
||||
| `sortSeq` | number? | 정렬순서 |
|
||||
| `useYn` | string? | 사용여부 |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
|
||||
**CklstRvwSaveRequest**: `cklstArtclSn`\*, `cklstSn`\*, `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
|
||||
**CklstRvwUpdateRequest**: `upCklstRvwSn?`, `rvwCnLvNo?`, `rvwCn?`, `bchckUseYn?`, `sortSeq?`, `useYn?`
|
||||
|
||||
**CklstRvwSearchParams**: `cklstSn?`, `cklstArtclSn?`, `useYn?` + `page`, `size`, `sort`
|
||||
|
||||
**CklstRvwId**: `{ cklstRvwSn: number; cklstArtclSn: number; cklstSn: number }`
|
||||
|
||||
**React Query hooks**: `useCklstRvwPage`, `useCklstRvwByArtcl`, `useCklstRvw`, `useCreateCklstRvw`, `useUpdateCklstRvw`, `useDeleteCklstRvw`
|
||||
|
||||
### 7-5. 공통코드 (보조)
|
||||
|
||||
| UI 필드 | 코드 그룹 (TBD) | 예시 값 |
|
||||
| -------- | --------------- | -------------------------------------------- |
|
||||
| 문서구분 | `ASDOCSECD` 등 | FNRS, INSTL, PWRK, DESIGN |
|
||||
| 업무구분 | TBD | RE(재이용수), SF(하수처리시설), SP(하수관로) |
|
||||
| 사용여부 | 고정 | Y, N |
|
||||
|
||||
공통코드 조회: `@/entities/code` — `codeService`, `CodeSelect`
|
||||
|
||||
---
|
||||
|
||||
## 8. UI 구현 가이드
|
||||
|
||||
### 8-1. 기술 스택·원칙
|
||||
|
||||
- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `RadioGroup` 등)
|
||||
- **테이블**: `@tanstack/react-table` + `useDataTable`
|
||||
- **데이터**: React Query — `@/entities/cklst` hooks (`useCklstRvwPage` 등 영역 C 포함)
|
||||
- **공통코드**: `@/entities/code` — `CodeSelect`, `findCodeLabel`
|
||||
- **상태**: 선택된 `cklstSn`, 영역 C 검토 행 배열(신규·dirty·계층 트리)
|
||||
- **모의 데이터**: API 연동 전 `@faker-js/faker`
|
||||
|
||||
### 8-2. 모듈·파일 배치(제안)
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/checklist-management/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/checklist-management/` (Public API: page만 export) |
|
||||
| UI | `.../ui/cklst-mng-list.tsx`, `cklst-mng-form-dialog.tsx`, `cklst-artcl-list.tsx`, `cklst-artcl-form-dialog.tsx`, `cklst-rvw-grid.tsx`, `cklst-preview-dialog.tsx` |
|
||||
| model | `.../model/cklst-mng-columns.tsx`, `cklst-artcl-columns.tsx`, `cklst-rvw-row-types.ts` |
|
||||
| lib | `.../lib/use-cklst-rvw-rows.ts`, `validators.ts`, `build-rvw-tree.ts` |
|
||||
| 엔티티 | `apps/as/src/entities/cklst/` (`cklstMng`, `cklstArtcl`, `cklstRvw` 서비스·hooks) |
|
||||
| 페이지 | `apps/as/src/pages/checklist-management/` — `/checklist-management` |
|
||||
| 문서 | `apps/as/docs/checklist-management/SPEC.md` |
|
||||
|
||||
### 8-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 주요 UI |
|
||||
| ---- | ---------------------------------------------------------------------------- |
|
||||
| A | DataTable + 행 선택 + 점검표 추가/수정 Dialog |
|
||||
| B | DataTable + 점검항목 추가/수정 Dialog |
|
||||
| C | 계층형 편집 그리드(1·2단계) + [추가][저장][미리보기] + 행별 [하위추가][삭제] |
|
||||
| 공통 | `CodeSelect` (문서구분·업무구분), 사용여부 Y/N Select/Radio |
|
||||
|
||||
### 8-4. 다이얼로그·라우팅
|
||||
|
||||
| 다이얼로그명 | 트리거 | API 연동 |
|
||||
| ------------------- | --------------- | ------------------------- |
|
||||
| 점검표 추가 | [점검표 추가] | `useCreateCklstMng` |
|
||||
| 점검표 수정 | 행 [관리] | `useUpdateCklstMng` |
|
||||
| 점검표 삭제 확인 | 행 [삭제] | `useDeleteCklstMng` |
|
||||
| 점검항목 추가 | [점검항목 추가] | `useCreateCklstArtcl` |
|
||||
| 점검항목 수정 | 행 [관리] | `useUpdateCklstArtcl` |
|
||||
| 체크리스트 미리보기 | [미리보기] | TBD — 읽기 전용 트리 렌더 |
|
||||
|
||||
라우트: `/checklist-management` (메뉴 URL은 CMS 등록값과 일치시킴)
|
||||
|
||||
### 8-5. 데이터 흐름(요약)
|
||||
|
||||
- **진입**: `useCklstMngPage` → 영역 A, `useCklstArtclPage` → 영역 B. C 비활성.
|
||||
- **점검표 선택**: `cklstSn` 설정 → 영역 C 활성 → `useCklstRvwPage({ cklstSn })` → 트리 그리드 표시.
|
||||
- **점검표 CRUD**: Dialog → `useCreateCklstMng` / `useUpdateCklstMng` / `useDeleteCklstMng`.
|
||||
- **점검항목 CRUD**: Dialog → `useCreateCklstArtcl` / `useUpdateCklstArtcl` / `useDeleteCklstArtcl`.
|
||||
- **검토 저장**: [저장] → 신규 POST / 기존 PUT 순차 호출 (일괄 API 없음).
|
||||
- **영역 C 점검항목 Select**: `useCklstArtclList(cklstSn)` 또는 영역 B 전체 목록과 교차 — **TBD**.
|
||||
|
||||
### 8-6. 미확정 / 검증 필요
|
||||
|
||||
- [ ] 영역 B 점검항목 등록 시 `cklstSn` 필수 vs UI 무소속 마스터 — 백엔드·기획 확인
|
||||
- [ ] `점검항목코드`(ITEM_001) 표시 형식 vs `cklstArtclSn` 숫자 PK
|
||||
- [ ] 영역 C 전체 검토 트리 조회: `cklstSn` 단위 일괄 API 부재 — `by-artcl` 다건 호출 또는 백엔드 확장
|
||||
- [x] `@/entities/cklst` — `cklstRvwService`·hooks 구현
|
||||
- [x] `pages/checklist-management` — 영역 A·B·C UI 구현 (`/checklist-management`)
|
||||
- [ ] 문서구분·업무구분 공통코드 상위코드 ID (`ASDOCSECD` 등)
|
||||
- [ ] [미리보기] 다이얼로그 UI·데이터 소스
|
||||
- [ ] `bchckUseYn` UI 반영 여부
|
||||
- [ ] 영역 A·B 페이지네이션·검색 UI 노출 여부
|
||||
- [ ] 권한(역할별 CRUD)
|
||||
@@ -1,385 +0,0 @@
|
||||
# 화면설계서: 코드관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 0.1 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 코드관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 코드관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
설치인가 등 AS 업무에서 참조하는 **공종구분코드**(공종코드)와 **회계구분코드**(회계코드)를 한 화면에서 조회·등록·수정·삭제한다. 좌측 **공종코드 관리**와 우측 **회계코드 관리**는 독립된 목록·CRUD 영역이며, 각 섹션은 자체 API(`cnstp-se`, `acntg-se`)와 연동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 코드관리 브레드크럼: 디지털행정지원 > 사이트관리 > 코드관리 │
|
||||
├──────────────────────────────────────────────────┬────────────────────────────────────────────────┤
|
||||
│ [영역 A] 공종코드 관리 │ [영역 B] 회계코드 관리 │
|
||||
│ [등록] │ [등록] │
|
||||
│ ┌────────────────────────────────────────────┐ │ ┌──────────────────────────────────────────┐ │
|
||||
│ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │ │ 코드 | 코드명 | 설명 | 사용여부 | 관리 │ │
|
||||
│ ├────────────────────────────────────────────┤ │ ├──────────────────────────────────────────┤ │
|
||||
│ │ WC001 공공하수처리시설 … 사용 [수정][삭제]│ │ │ AC101 시설비 … 사용 [수정][삭제] │ │
|
||||
│ │ WC002 하수관로정비 … 사용 [수정][삭제]│ │ │ AC102 용역비 … 사용 [수정][삭제] │ │
|
||||
│ │ WC003 재이용시설 … 사용 [수정][삭제]│ │ │ AC103 운영비 … 미사용 [수정][삭제] │ │
|
||||
│ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────┘ │
|
||||
│ « 1 2 3 … » │ « 1 2 3 … » │
|
||||
└──────────────────────────────────────────────────┴────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입 — 좌·우 독립 조회
|
||||
CALL cnstpSeMngService.findAll({ page, size })
|
||||
DISPLAY 공종코드 그리드 (영역 A)
|
||||
|
||||
CALL acntgSeMngService.findAll({ page, size })
|
||||
DISPLAY 회계코드 그리드 (영역 B)
|
||||
|
||||
// 영역 A: 공종코드 등록
|
||||
ON 영역 A [등록] 버튼 CLICK DO
|
||||
OPEN 공종코드 등록 다이얼로그
|
||||
ON [저장] DO
|
||||
VALIDATE cnstpSeCd
|
||||
CALL cnstpSeMngService.save(CnstpSeMngSaveRequestDto)
|
||||
CALL cnstpSeMngService.findAll
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
ON [취소] / [닫기] DO
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 A: 공종코드 수정
|
||||
ON 영역 A 행 [수정] CLICK DO
|
||||
CALL cnstpSeMngService.findById(cnstpSeCd)
|
||||
OPEN 공종코드 수정 다이얼로그 (기존 값 초기화)
|
||||
ON [저장] DO
|
||||
CALL cnstpSeMngService.update(cnstpSeCd, CnstpSeMngSaveRequestDto)
|
||||
CALL cnstpSeMngService.findAll
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 A: 공종코드 삭제
|
||||
ON 영역 A 행 [삭제] CLICK DO
|
||||
CONFIRM "선택한 공종코드를 삭제하시겠습니까?"
|
||||
CALL cnstpSeMngService.delete(cnstpSeCd)
|
||||
CALL cnstpSeMngService.findAll
|
||||
END ON
|
||||
|
||||
// 영역 B: 회계코드 등록
|
||||
ON 영역 B [등록] 버튼 CLICK DO
|
||||
OPEN 회계코드 등록 다이얼로그
|
||||
ON [저장] DO
|
||||
VALIDATE acntgSeCd, acntgSeCdNm
|
||||
CALL acntgSeMngService.save(AcntgSeMngSaveRequestDto)
|
||||
CALL acntgSeMngService.findAll
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
ON [취소] / [닫기] DO
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B: 회계코드 수정
|
||||
ON 영역 B 행 [수정] CLICK DO
|
||||
CALL acntgSeMngService.findById(acntgSeCd)
|
||||
OPEN 회계코드 수정 다이얼로그 (기존 값 초기화)
|
||||
ON [저장] DO
|
||||
CALL acntgSeMngService.update(acntgSeCd, AcntgSeMngSaveRequestDto)
|
||||
CALL acntgSeMngService.findAll
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B: 회계코드 삭제
|
||||
ON 영역 B 행 [삭제] CLICK DO
|
||||
CONFIRM "선택한 회계코드를 삭제하시겠습니까?"
|
||||
CALL acntgSeMngService.delete(acntgSeCd)
|
||||
CALL acntgSeMngService.findAll
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A: 공종코드 관리
|
||||
|
||||
백엔드 **공종구분코드**(`CnstpSeMng`) CRUD. 계층 구조(`upCnstpCd`, `cdLvNo`)를 지원하나, 본 화면은 **평면 목록**으로 표시.
|
||||
|
||||
### 4-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------- |
|
||||
| 섹션 타이틀 | 공종코드 관리 |
|
||||
| 우측 버튼 | **[등록]** — 공종코드 등록 다이얼로그 |
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| -------- | ------------- | ------ | ------------------------ |
|
||||
| 코드 | `cnstpSeCd` | string | PK, 최대 10자 |
|
||||
| 코드명 | `cnstpSeCdNm` | string | 최대 100자 |
|
||||
| 설명 | `expln` | string | 최대 1000자, 선택 |
|
||||
| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||
|
||||
- 행 클릭 시 별도 상세 이동 없음. 수정은 **[수정]** 버튼으로 다이얼로그 진입.
|
||||
- 페이지네이션: Spring `Pageable` (`page`, `size`, `sort`) — **UI 노출**. 기본 `size: 10`, `@common/components` 공통 페이지네이션 컴포넌트 사용.
|
||||
|
||||
### 4-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ------ | -------------- | ---------------------------------------- |
|
||||
| 등록 | 섹션 우측 상단 | 공종코드 등록 다이얼로그 오픈 |
|
||||
| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 |
|
||||
|
||||
### 4-4. 다이얼로그: 공종코드 등록 / 수정
|
||||
|
||||
다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다.
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- |
|
||||
| 코드 | Input | Y | — | `cnstpSeCd` | 등록 시 입력, 수정 시 읽기 전용 |
|
||||
| 코드명 | Input | N | — | `cnstpSeCdNm` | 최대 100자 |
|
||||
| 설명 | Input | N | — | `expln` | 최대 1000자 |
|
||||
| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 |
|
||||
|
||||
> UI에 노출하지 않는 API 필드(`upCnstpCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지.
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ------------------------------------------------ |
|
||||
| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 |
|
||||
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
|
||||
| 닫기 | 다이얼로그 닫기 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B: 회계코드 관리
|
||||
|
||||
백엔드 **회계구분관리**(`AcntgSeMng`) CRUD. 영역 A와 동일한 **평면 목록** UI 패턴을 따른다.
|
||||
|
||||
### 5-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------- |
|
||||
| 섹션 타이틀 | 회계코드 관리 |
|
||||
| 우측 버튼 | **[등록]** — 회계코드 등록 다이얼로그 |
|
||||
|
||||
### 5-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| -------- | ------------- | ------ | ------------------------ |
|
||||
| 코드 | `acntgSeCd` | string | PK, 최대 10자 |
|
||||
| 코드명 | `acntgSeCdNm` | string | 최대 100자 |
|
||||
| 설명 | `expln` | string | 최대 1000자, 선택 |
|
||||
| 사용여부 | `useYn` | string | `Y`→사용, `N`→미사용 |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||
|
||||
- 페이지네이션: 영역 A와 동일 — **UI 노출**, 기본 `size: 10`.
|
||||
|
||||
### 5-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ------ | -------------- | ---------------------------------------- |
|
||||
| 등록 | 섹션 우측 상단 | 회계코드 등록 다이얼로그 오픈 |
|
||||
| 수정 | 행별 | 상세 조회 후 수정 다이얼로그 (값 초기화) |
|
||||
| 삭제 | 행별 | 확인 후 삭제, 목록 갱신 |
|
||||
|
||||
### 5-4. 다이얼로그: 회계코드 등록 / 수정
|
||||
|
||||
다이얼로그 입력 필드는 **코드 · 코드명 · 설명 · 사용여부** 4개만 노출한다. (영역 A와 동일 패턴)
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | --------- | ---- | ------ | ----------------- | ------------------------------- |
|
||||
| 코드 | Input | Y | — | `acntgSeCd` | 등록 시 입력, 수정 시 읽기 전용 |
|
||||
| 코드명 | Input | Y | — | `acntgSeCdNm` | 최대 100자 |
|
||||
| 설명 | Input | N | — | `expln` | 최대 1000자 |
|
||||
| 사용여부 | Radio | N | Y | `useYn` | `Y`→사용, `N`→미사용 |
|
||||
|
||||
> UI에 노출하지 않는 API 필드(`upAcntgSeCd`, `cdLvNo`): 등록 시 `cdLvNo: 1` 등 기본값 전송, 수정 시 `findById` 응답값 유지.
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ------------------------------------------------ |
|
||||
| 저장 | POST(등록) 또는 PUT(수정) 후 `findAll` 목록 갱신 |
|
||||
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
|
||||
| 닫기 | 다이얼로그 닫기 |
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ---------------------------------------------------------------------------------- |
|
||||
| 서비스 | `as-service` |
|
||||
| Base prefix | `/as-service/api/v1` (`ENDPOINT.AS_COMMON_SERVICE`) |
|
||||
| 인증 | JWT (`Authorization` 헤더), `X-Site-Id`, `token-id` |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 엔티티 | `@/entities/cnstp` (`cnstpSeMngService`), `@/entities/acntg` (`acntgSeMngService`) |
|
||||
|
||||
**부가 API (본 화면 목록 외 참조용)**
|
||||
|
||||
| 리소스 | Method | URL | Response | 용도 |
|
||||
| ------------ | ------ | --------------------- | --------------------- | --------------------------------- |
|
||||
| 공종구분코드 | GET | `/cnstp-se/list/tree` | `CnstpSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) |
|
||||
| 회계구분관리 | GET | `/acntg-se/list/tree` | `AcntgSeMngTreeDto[]` | 계층 트리 (타 화면·상위코드 선택) |
|
||||
|
||||
### 6-2. 공종구분코드 (영역 A) — `cnstp-se`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- |
|
||||
| 목록 조회 | GET | `/cnstp-se` | `CnstpSeMngSearchDto` + Pageable | `Page<CnstpSeMngResponseDto>` | `cnstpSeMngService.findAll` |
|
||||
| 트리 조회 | GET | `/cnstp-se/list/tree` | — | `CnstpSeMngTreeDto[]` | `cnstpSeMngService.findTree` |
|
||||
| 상세 조회 | GET | `/cnstp-se/{cnstpSeCd}` | — | `CnstpSeMngResponseDto` | `cnstpSeMngService.findById` |
|
||||
| 등록 | POST | `/cnstp-se` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.save` |
|
||||
| 수정 | PUT | `/cnstp-se/{cnstpSeCd}` | `CnstpSeMngSaveRequestDto` | `string` (cnstpSeCd) | `cnstpSeMngService.update` |
|
||||
| 삭제 | DELETE | `/cnstp-se/{cnstpSeCd}` | — | `string` (cnstpSeCd) | `cnstpSeMngService.delete` |
|
||||
|
||||
**CnstpSeMngResponseDto**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------- | ------- | ------------------ |
|
||||
| `cnstpSeCd` | string | 공종구분코드 |
|
||||
| `cnstpSeCdNm` | string | 공종구분코드명 |
|
||||
| `upCnstpCd` | string? | 상위공종코드 |
|
||||
| `cdLvNo` | number | 코드레벨번호 |
|
||||
| `useYn` | string | 사용여부 |
|
||||
| `expln` | string? | 설명 (최대 1000자) |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**CnstpSeMngSaveRequestDto**: `cnstpSeCd`, `cnstpSeCdNm?`, `upCnstpCd?`, `cdLvNo?`, `useYn?`, `expln?`
|
||||
|
||||
**CnstpSeMngSearchDto**: `cnstpSeCd?`, `cnstpSeCdNm?`, `useYn?` + `page`, `size`, `sort`
|
||||
|
||||
**React Query hooks**: `useCnstpSeMngPage`, `useCnstpSeMngTree`, `useCnstpSeMng`, `useSaveCnstpSeMng`, `useUpdateCnstpSeMng`, `useDeleteCnstpSeMng`
|
||||
|
||||
### 6-3. 회계구분관리 (영역 B) — `acntg-se`
|
||||
|
||||
| 기능 | Method | URL | Request / Query | Response | 클라이언트 |
|
||||
| --------- | ------ | ----------------------- | -------------------------------- | ----------------------------- | ---------------------------- |
|
||||
| 목록 조회 | GET | `/acntg-se` | `AcntgSeMngSearchDto` + Pageable | `Page<AcntgSeMngResponseDto>` | `acntgSeMngService.findAll` |
|
||||
| 트리 조회 | GET | `/acntg-se/list/tree` | — | `AcntgSeMngTreeDto[]` | `acntgSeMngService.findTree` |
|
||||
| 상세 조회 | GET | `/acntg-se/{acntgSeCd}` | — | `AcntgSeMngResponseDto` | `acntgSeMngService.findById` |
|
||||
| 등록 | POST | `/acntg-se` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.save` |
|
||||
| 수정 | PUT | `/acntg-se/{acntgSeCd}` | `AcntgSeMngSaveRequestDto` | `string` (acntgSeCd) | `acntgSeMngService.update` |
|
||||
| 삭제 | DELETE | `/acntg-se/{acntgSeCd}` | — | `string` (acntgSeCd) | `acntgSeMngService.delete` |
|
||||
|
||||
**AcntgSeMngResponseDto**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------- | ------- | ------------------ |
|
||||
| `acntgSeCd` | string | 회계구분코드 |
|
||||
| `acntgSeCdNm` | string | 회계구분코드명 |
|
||||
| `upAcntgSeCd` | string? | 상위회계구분코드 |
|
||||
| `cdLvNo` | number | 코드레벨번호 |
|
||||
| `useYn` | string | 사용여부 |
|
||||
| `expln` | string? | 설명 (최대 1000자) |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**AcntgSeMngSaveRequestDto**: `acntgSeCd`, `acntgSeCdNm`, `upAcntgSeCd?`, `cdLvNo`, `useYn?`, `expln?`
|
||||
|
||||
**AcntgSeMngSearchDto**: `acntgSeCd?`, `acntgSeCdNm?`, `useYn?` + `page`, `size`, `sort`
|
||||
|
||||
**React Query hooks**: `useAcntgSeMngPage`, `useAcntgSeMngTree`, `useAcntgSeMng`, `useSaveAcntgSeMng`, `useUpdateAcntgSeMng`, `useDeleteAcntgSeMng`
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- Next.js App Router, React Query (`@tanstack/react-query`)
|
||||
- `@common/components` — `Card`, `Button`, `Dialog`, `DataTable` / `useDataTable`
|
||||
- `@tanstack/react-table` — 컬럼 정의는 `model/*-table-columns.tsx` 분리
|
||||
- 기존 entity 레이어 재사용: `@/entities/cnstp`, `@/entities/acntg` (service·queries·interface 구현 완료)
|
||||
- 좌·우 2열 레이아웃: `grid grid-cols-1 lg:grid-cols-2 gap-4` (모바일 1열 스택)
|
||||
- 삭제 전 `confirm` / Toast 피드백 — 프로젝트 공통 패턴 준수
|
||||
- 목록 하단 **페이지네이션 UI 노출** — 영역 A·B 각각 독립 page state
|
||||
- **검색 필터 UI 미제공** — `findAll` 호출 시 `page`, `size`만 전달
|
||||
- **등록/수정 다이얼로그** — 코드·코드명·설명은 `Input`, 사용여부는 `Radio` (4필드만 노출)
|
||||
- 상위 코드 삭제 시 **하위 코드 연쇄 삭제 UI·로직 미구현** — 단건 삭제만 수행
|
||||
|
||||
### 7-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 (제안) |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/src/app/(authenticated)/site/code-management/page.tsx` |
|
||||
| 페이지 컴포넌트 | `apps/as/src/pages/code-management/ui/code-management-page.tsx` |
|
||||
| 공종 그리드 컬럼 | `apps/as/src/pages/code-management/model/cnstp-se-table-columns.tsx` |
|
||||
| 회계 그리드 컬럼 | `apps/as/src/pages/code-management/model/acntg-se-table-columns.tsx` |
|
||||
| 공종 등록/수정 폼 | `apps/as/src/pages/code-management/ui/cnstp-se-form-dialog.tsx` |
|
||||
| 회계 등록/수정 폼 | `apps/as/src/pages/code-management/ui/acntg-se-form-dialog.tsx` |
|
||||
| API·타입 (기존) | `@/entities/cnstp`, `@/entities/acntg` |
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | UI 컴포넌트 | 데이터 소스 | 비고 |
|
||||
| ------------ | ------------------------------------- | ------------------------------------------- | ------------------------------ |
|
||||
| A | `CnstpSeSection` + Table + Pagination | `useCnstpSeMngPage({ page, size })` | 독립 page state·mutation |
|
||||
| B | `AcntgSeSection` + Table + Pagination | `useAcntgSeMngPage({ page, size })` | A와 상호 의존 없음 |
|
||||
| A 다이얼로그 | `CnstpSeFormDialog` | `useCnstpSeMng`, `useSave/UpdateCnstpSeMng` | Input 4필드 + Radio 사용여부 |
|
||||
| B 다이얼로그 | `AcntgSeFormDialog` | `useAcntgSeMng`, `useSave/UpdateAcntgSeMng` | Input 4필드 + Radio 사용여부 |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 다이얼로그 | 트리거 | 라우팅 |
|
||||
| ------------- | ---------------- | -------------------- |
|
||||
| 공종코드 등록 | 영역 A [등록] | URL 변경 없음 (모달) |
|
||||
| 공종코드 수정 | 영역 A 행 [수정] | URL 변경 없음 (모달) |
|
||||
| 회계코드 등록 | 영역 B [등록] | URL 변경 없음 (모달) |
|
||||
| 회계코드 수정 | 영역 B 행 [수정] | URL 변경 없음 (모달) |
|
||||
|
||||
### 7-5. 데이터 흐름 (구현 관점)
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 페이지 마운트 — 검색 조건 없이 page·size만 전달
|
||||
const cnstpQuery = useCnstpSeMngPage({ page: 0, size: 20 })
|
||||
const acntgQuery = useAcntgSeMngPage({ page: 0, size: 20 })
|
||||
|
||||
DISPLAY CodeManagementPage
|
||||
├─ CnstpSeSection(data=cnstpQuery.data, pagination)
|
||||
└─ AcntgSeSection(data=acntgQuery.data, pagination)
|
||||
|
||||
// 페이지 변경
|
||||
ON pagination page/size CHANGE DO
|
||||
refetch cnstpQuery | acntgQuery with new page, size
|
||||
END ON
|
||||
|
||||
// mutation 성공 시
|
||||
ON save/update/delete SUCCESS DO
|
||||
invalidateQueries cnstp-se | acntg-se baseKey
|
||||
TOAST "저장되었습니다." | "삭제되었습니다."
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -1,498 +0,0 @@
|
||||
# 화면설계서: 전국 행정지원 지도 (대시보드 메인)
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 1.2 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 전국 행정지원 지도 (대시보드 메인) |
|
||||
| 화면 ID | TBD |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인 사용자의 **기관 권한·당해 연도** 기준으로 문서별 진행상태 통계와 행정지원 알림을 한 화면에서 조회한다. 좌측 지도에서 전국 행정지원 현황을 보고, 우측에서 진행상황 카드·알림이력·공지사항을 확인한 뒤 알림 [처리하기]로 연계 업무 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
브레드크럼·페이지 타이틀은 스크린샷에 없음. 홈(`/`) 메인 콘텐츠로 구성한다.
|
||||
|
||||
```
|
||||
┌────────────────────────────────┬─────────────────────────────────────────────────┐
|
||||
│ [영역 A] 전국 행정지원 지도 │ [영역 B] 문서별 진행상황 카드 (2×2) │
|
||||
│ 타이틀: 전국 행정지원 지도 │ ┌────────────────┐ ┌────────────────┐ │
|
||||
│ │ │ 재원협의 │ │ 설치인가 │ │
|
||||
│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
|
||||
│ 마커: 서울 강남구, │ ├────────────────┤ ├────────────────┤ │
|
||||
│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │
|
||||
│ 부산 해운대구 │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │
|
||||
│ │ └────────────────┘ └────────────────┘ │
|
||||
│ ├─────────────────────────────────────────────────┤
|
||||
│ │ [영역 C] 알림이력 │
|
||||
│ │ 알림이력 업데이트 HH:mm [전체보기] │
|
||||
│ │ [오늘 N건] [지연 N건] [완료대기 N건] │
|
||||
│ │ [뱃지] [일자] 제목 [처리하기] │
|
||||
│ │ … (최대 5건) │
|
||||
│ ├─────────────────────────────────────────────────┤
|
||||
│ │ [영역 D] 공지사항 [더보기] │
|
||||
│ │ ┌────┬────────┬────────┬────────┬──────┐ │
|
||||
│ │ │번호│ 제목 │ 작성자 │ 작성일 │조회수│ │
|
||||
│ │ └────┴────────┴────────┴────────┴──────┘ │
|
||||
└────────────────────────────────┴─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// searchYear 미전달 → 서버가 당해 연도로 고정. 알림 목록은 최신 5건
|
||||
CALL getMainDashboard(DashboardMainSearchDto) // Authorization은 client 인터셉터
|
||||
DISPLAY 진행상황 카드 (영역 B) // docPrgrsSttsList, TOTAL 행 제외
|
||||
DISPLAY 알림 통계 칩 · 알림 목록 (영역 C) // notiStats, notiList
|
||||
|
||||
CALL getUserPostList(UserPostSearchParams) // board-service, AS_DASHBOARD_BBS_ID
|
||||
DISPLAY 공지사항 그리드 (영역 D)
|
||||
|
||||
CALL 지도 마커/음영 조회(…) // TBD — 대시보드 API에 없음
|
||||
DISPLAY 전국 지도 (영역 A)
|
||||
|
||||
ON 영역 C [전체보기] 버튼 CLICK DO
|
||||
NAVIGATE /nti-hstry
|
||||
END ON
|
||||
|
||||
ON 영역 C [처리하기] 버튼 CLICK DO
|
||||
CALL readNoti(notiId) // entities/noti
|
||||
NAVIGATE notiUrl // 비어 있으면 이동하지 않음
|
||||
CALL getMainDashboard(…) // → 영역 C 갱신 (invalidate)
|
||||
END ON
|
||||
|
||||
ON 영역 D [더보기] 버튼 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID} // as 앱 공지 목록 라우트 미구현. st/cm 관례
|
||||
END ON
|
||||
|
||||
ON 영역 D 행 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
|
||||
END ON
|
||||
|
||||
ON 영역 B 카드 CLICK DO
|
||||
NAVIGATE 해당 문서 목록 // §5-3
|
||||
END ON
|
||||
|
||||
ON 영역 A 마커 CLICK DO
|
||||
// TBD — 팝업/필터/이동 여부 미확정
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 전국 행정지원 지도
|
||||
|
||||
스크린샷 타이틀: **전국 행정지원 지도**.
|
||||
|
||||
`getMainDashboard` 응답에 지도·좌표·행정구역 음영 데이터는 **없다**. 마커·음영 소스 API는 **TBD**.
|
||||
|
||||
### 4-1. 표시 요소
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ----------- | --------- | ---- | ------ | ----------------- | ----------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | Text | — | — | — | 고정 문구 `전국 행정지원 지도` |
|
||||
| 전국 지도 | Map | — | — | TBD | 시도·시군구 경계. 북·동부 음영(청) / 남서부 비음영(백) — 의미 TBD |
|
||||
| 마커 라벨 | Marker | — | — | TBD | 스크린샷 샘플: `서울 강남구`, `대전 유성구`, `부산 해운대구` |
|
||||
|
||||
### 4-2. 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| 마커 클릭 | TBD (상세 팝업 / 해당 지자체 필터 / 이동) |
|
||||
| 지도 줌·팬 | TBD (스크린샷에 컨트롤 없음) |
|
||||
|
||||
`entities/adstdstr`는 시도·구군 **콤보**만 제공하며 좌표를 주지 않는다. 지도 라이브러리도 as 앱에 없음 — 선정 TBD.
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 문서별 진행상황 카드
|
||||
|
||||
`DashboardMainResponseDto.docPrgrsSttsList`를 **`docSeComCd`로 매핑**해 4장 카드를 그린다. 배열 순서에 의존하지 않는다.
|
||||
|
||||
스크린샷 배치(좌→우, 상→하):
|
||||
|
||||
| 화면 카드 타이틀 | `docSeComCd` | API `docSeNm` |
|
||||
| ----------------- | ------------ | -------------- |
|
||||
| 재원협의 진행상황 | `FNRS` | 재원협의 |
|
||||
| 설치인가 진행상황 | `INSTL` | 설치인가 |
|
||||
| 설계자문 진행상황 | `CNFM` | 설계자문 |
|
||||
| 기본계획 진행상황 | `SEWER` | 하수도기본계획 |
|
||||
|
||||
서버 SQL `ORDER BY`는 `INSTL → FNRS → SEWER → CNFM` 이며, `GROUP BY ROLLUP`으로 **`docSeComCd = TOTAL`** 합계 행이 추가된다. 카드에는 TOTAL 행을 **표시하지 않는다**.
|
||||
|
||||
카드 타이틀은 스크린샷 문구(`… 진행상황`)를 쓴다. `SEWER`의 API명 `하수도기본계획`은 화면에서 `기본계획`으로 축약한다.
|
||||
|
||||
### 5-1. 카드 내부 필드
|
||||
|
||||
각 카드 공통 구조. `totCnt`는 화면에 없음.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | ---------- | ------------------ |
|
||||
| 신청 | `reqCnt` | 파란 아이콘 + 건수 |
|
||||
| 진행 | `prgrsCnt` | 초록 아이콘 + 건수 |
|
||||
| 완료 | `cmptnCnt` | 주황 아이콘 + 건수 |
|
||||
|
||||
상태 그룹(서버): 임시저장(`SV`/`CV`) 제외. 문서별 `prgrs_stts_com_cd` → `REQ`/`PRG`/`CMP`.
|
||||
|
||||
### 5-2. 검색 파라미터 (화면 미표시)
|
||||
|
||||
영역 B에 연도·필터 UI는 없다. 서버 기본값을 따른다.
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ------ | --------- | ---- | --------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| (없음) | — | — | 당해 연도 | `searchYear` | 미전달 시 서버 `LocalDate.now().getYear()`. 연도 셀렉터 추가 여부 TBD |
|
||||
|
||||
`docSeComCd` 쿼리는 **알림 목록 필터**에만 쓰이며, 진행상황 SQL에는 전달되지 않는다. 카드 4종은 항상 전원 집계한다.
|
||||
|
||||
### 5-3. 상호작용
|
||||
|
||||
카드 클릭 시 **해당 문서 목록 페이지**로 이동한다. `docSeComCd` → 라우트 고정 매핑.
|
||||
|
||||
| 동작 | `docSeComCd` | 이동 경로 |
|
||||
| --------- | ------------ | ----------------- |
|
||||
| 카드 클릭 | `FNRS` | `/fnrs-cnsltn` |
|
||||
| 카드 클릭 | `INSTL` | `/instl-sncn` |
|
||||
| 카드 클릭 | `CNFM` | `/design-cnstn` |
|
||||
| 카드 클릭 | `SEWER` | `/sewer-bsc-plan` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 영역 C — 알림이력
|
||||
|
||||
서버가 cms-service 알림을 대시보드 응답에 합친다. 목록은 **page=0, size=5** (최신 5건). 페이지네이션 없음.
|
||||
|
||||
### 6-1. 헤더
|
||||
|
||||
| 표시명 | Field / 소스 | 비고 |
|
||||
| ------------- | ------------ | --------------------------------------------------------------------------------------- |
|
||||
| 섹션 타이틀 | — | 고정 문구 `알림이력` |
|
||||
| 업데이트 시각 | (client) | 스크린샷 `업데이트 10:32`. API 필드 없음. `getMainDashboard` 마지막 fetch 시각(`HH:mm`) |
|
||||
| 전체보기 | Button | `/nti-hstry`로 이동 |
|
||||
|
||||
### 6-2. 통계 칩
|
||||
|
||||
`NotiAsStatResponseDto`.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------------ | -------------- | -------------------------------------- |
|
||||
| 오늘 N건 | `todayCount` | 당일 00:00 이후 수신 |
|
||||
| 지연 N건 | `delayCount` | `notReadYn=N` 이고 수신 후 3일 이상 |
|
||||
| 완료대기 N건 | `pendingCount` | `notReadYn=Y` 이고 `notiPrcsCmptnYn=N` |
|
||||
|
||||
### 6-3. 목록 행
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------- | --------------------------------------------- |
|
||||
| 일자 | `regDt` | 표시 `YYYY-MM-DD`. 응답 `yyyy-MM-dd HH:mm:ss` |
|
||||
| 제목 | `notiTtl` | 알림 제목 |
|
||||
| 처리 | (action) | 스크린샷 라벨 `처리하기`. 클릭 §6-4 |
|
||||
|
||||
응답에 있으나 이 목록에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| `notiId` | 행 키 · `readNoti` 파라미터 |
|
||||
| `recvUnqId` | 수신 회원 UUID (서버 로그인 사용자 고정) |
|
||||
| `senderUnqId` | 발신 회원 UUID |
|
||||
| `notiType` | `ADSS` |
|
||||
| `notiCn` | 본문 |
|
||||
| `notiUrl` | 처리 후 이동 URL. 컬럼 미표시 |
|
||||
| `notiLnkgUnqNo` | 알림연결고유번호 |
|
||||
| `docSeComCd` | 문서구분 코드 |
|
||||
| `docSeComNm` | 문서구분명 (알림이력 화면에서는 연계업무) |
|
||||
| `notReadYn` | 뱃지 판정 TBD에 사용 가능 |
|
||||
| `notiIdntyYn` | 확인 여부 |
|
||||
| `notiIdntyDt` | 확인 일시 |
|
||||
| `notiPrcsCmptnYn` | 처리 완료 여부 |
|
||||
| `notiPrcsCmptnDt` | 처리 완료 일시 |
|
||||
|
||||
### 6-4. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| [전체보기] 클릭 | `/nti-hstry` (알림이력 목록)로 이동 |
|
||||
| [처리하기] 클릭 | `readNoti(notiId)` 후 `notiUrl`로 이동. URL이 비면 이동하지 않음(읽음 처리는 수행). `dashboardBaseKey` invalidate |
|
||||
|
||||
`DashboardController`에 읽음 API는 없다. **`entities/noti`의 `readNoti`** 를 쓴다. 알림이력 화면과 동일 (`PUT /cms-service/api/v1/noti/{notiId}/read`).
|
||||
|
||||
### 6-5. 검색 파라미터 (화면 미표시)
|
||||
|
||||
서버 `toNotiSearchDto()` 기본: `notiType = ADSS`. 화면 필터 없음.
|
||||
|
||||
| DashboardMainSearchDto | 화면 | 비고 |
|
||||
| ---------------------- | ---- | --------------------------------- |
|
||||
| `notiType` | 없음 | 서버 기본 `ADSS` |
|
||||
| `notReadYn` | 없음 | 통계 칩 클릭 필터 시 후보 (TBD) |
|
||||
| `startDt` / `endDt` | 없음 | 대시보드 기본 미전달 |
|
||||
| `docSeComCd` | 없음 | 알림만 필터. 진행상황 카드와 무관 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 영역 D — 공지사항
|
||||
|
||||
`getMainDashboard`에 공지 필드는 없다. **board-service** `getUserPostList`로 별도 조회한다.
|
||||
|
||||
- Base: `GET /board-service/api/v1/posts/user/{AS_DASHBOARD_BBS_ID}`
|
||||
- `AS_DASHBOARD_BBS_ID` = `NEXT_PUBLIC_AS_DASHBOARD_BBS_ID` (로컬 예: `7`)
|
||||
- 클라이언트: `@/entities/board` — `postApi.getUserPostList` / `useUserPostList`
|
||||
|
||||
페이지네이션 컨트롤은 없다. 요약 목록만 표시한다 (`page=0`, `size=3`).
|
||||
|
||||
### 7-1. 헤더·버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ------------------------------------------------------------------------------------------- |
|
||||
| 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` 로 이동. as 앱에 공지 목록 라우트는 없음(st/cm 관례) |
|
||||
|
||||
### 7-2. 그리드 컬럼
|
||||
|
||||
스크린샷 헤더 순서. `UserPost`.
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------- | ---------------------------------------------------- |
|
||||
| 번호 | `rowNo` | `upndFixYn` = `Y` 이면 `[공지]` Badge, 그 외 `rowNo` |
|
||||
| 제목 | `pstTtl` | truncate |
|
||||
| 작성자 | `wrtrNm` | |
|
||||
| 작성일 | `regDt` | 표시 `YYYY-MM-DD` (응답 datetime) |
|
||||
| 조회수 | `inqNocs` | 숫자 |
|
||||
|
||||
조회 파라미터 (`UserPostSearchParams`):
|
||||
|
||||
| 필드 | 값 | 비고 |
|
||||
| ------------- | ----------- | ------------------------------------------ |
|
||||
| `page` | `0` | Spring 0-based |
|
||||
| `size` | `3` | 대시보드 요약. 스크린샷 3행 |
|
||||
| `keywordType` | `bbsTypeCd` | st 대시보드 공지 패널과 동일 |
|
||||
| `keyword` | `BBSNOTICE` | 게시판 유형. path의 bbsId와 함께 공지 한정 |
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**: `bbsId`, `pstNo`(행 키·상세 이동), `pstCn`, `upndFixYn`(번호 열 판정만), `rgtrNm`, `atflCd` 등.
|
||||
|
||||
### 7-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ------- | ------------------------------------------------------------------------------ |
|
||||
| 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` (as 앱 공지 상세 라우트 미구현) |
|
||||
|
||||
---
|
||||
|
||||
## 8. API/DTO 명세
|
||||
|
||||
### 8-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| Base prefix | `/as-service/api/v1/dashboard` (`ENDPOINT.AS_SERVICE.DASHBOARD`) |
|
||||
| 인증 | `Authorization` 헤더. 프론트는 `client` 인터셉터. 로그인 기관 권한은 서버가 사용자 캐시로 적용 |
|
||||
| 클라이언트 모듈 | `@/entities/dashboard` — `dashboardService` / `useGetMainDashboard` |
|
||||
| 알림 읽음 | `@/entities/noti` — `notiService.readNoti` / `useReadNoti` |
|
||||
| 공지사항 | `@/entities/board` — `postApi.getUserPostList` / `useUserPostList` |
|
||||
|
||||
영역 A(지도)는 본 컨트롤러·board-service 범위 밖 → **TBD**.
|
||||
|
||||
### 8-2. 대시보드 메인 조회 — `getMainDashboard`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------------------------------------- | ------ | ------------- | ------------------------------- | -------------------------- | ----------------------------------------------------------- |
|
||||
| 대시보드 메인 통합진행상태 및 알림 정보 조회 | GET | `{BASE}/main` | Query: `DashboardMainSearchDto` | `DashboardMainResponseDto` | `dashboardService.getMainDashboard` / `useGetMainDashboard` |
|
||||
|
||||
**Request (`DashboardMainSearchDto`)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------ | ---- | ---------------------------------------------------------------------- |
|
||||
| `searchYear` | N | 검색 연도. 미전달 시 서버 당해 연도. 영역 B 집계 기간 |
|
||||
| `notiType` | N | 알림 종류. 미전달 시 서버 `ADSS` |
|
||||
| `notReadYn` | N | 알림 확인 여부 `Y`/`N`. 본 화면 미사용 |
|
||||
| `startDt` | N | 알림 시작일 `YYYY-MM-DD`. 본 화면 미사용 |
|
||||
| `endDt` | N | 알림 종료일 `YYYY-MM-DD`. 본 화면 미사용 |
|
||||
| `docSeComCd` | N | 알림 문서구분 (`INSTL`/`FNRS`/`SEWER`/`CNFM` 등). 진행상황 집계와 무관 |
|
||||
|
||||
`Authorization`은 service 시그니처에 넣지 않는다.
|
||||
|
||||
**Response (`DashboardMainResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ------------------ | --------------------------- | --------------------------- |
|
||||
| `docPrgrsSttsList` | `DocPrgrsSttsResponseDto[]` | 문서별 진행상태 + TOTAL 행 |
|
||||
| `notiList` | `NotiAsUserResponseDto[]` | 최신 최대 5건 |
|
||||
| `notiStats` | `NotiAsStatResponseDto` | 오늘 / 지연 / 완료대기 건수 |
|
||||
|
||||
**`DocPrgrsSttsResponseDto`**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ------------ | ------ | --------------------------------------------- |
|
||||
| `docSeNm` | string | 문서 구분 명칭 |
|
||||
| `docSeComCd` | string | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `TOTAL` |
|
||||
| `reqCnt` | number | 신청 건수 |
|
||||
| `prgrsCnt` | number | 진행 건수 |
|
||||
| `cmptnCnt` | number | 완료 건수 |
|
||||
| `totCnt` | number | 총 건수 (화면 미표시) |
|
||||
|
||||
**`NotiAsStatResponseDto`**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| -------------- | ------ | --------- |
|
||||
| `todayCount` | number | 오늘 알림 |
|
||||
| `delayCount` | number | 지연 알림 |
|
||||
| `pendingCount` | number | 완료대기 |
|
||||
|
||||
**`NotiAsUserResponseDto`** — 필드 상세는 `docs/spec/nti-hstry` §6-2와 동일 (`notiId`, `notiTtl`, `regDt`, `notiUrl`, `notReadYn`, `notiPrcsCmptnYn` 등).
|
||||
|
||||
### 8-3. 알림 읽음 처리 — `readNoti`
|
||||
|
||||
대시보드 컨트롤러가 아니라 cms-service. 영역 C [처리하기]에서 호출.
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------- | ------ | ---------------------------------------- | ----------------------- | ---------------- | -------------------------------------- |
|
||||
| 알림 읽음 처리 | PUT | `/cms-service/api/v1/noti/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
|
||||
|
||||
Body 없음. 성공 시 `dashboardBaseKey`(및 필요 시 `notiBaseKey`) invalidate.
|
||||
|
||||
### 8-4. 전국 행정지원 지도 — TBD
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ------------------- | ------ | --- | ----------- | ------------ | --- |
|
||||
| 지도 마커·음영 조회 | TBD | TBD | TBD | TBD | TBD |
|
||||
|
||||
### 8-5. 공지사항 — `getUserPostList`
|
||||
|
||||
board-service. bbsId는 환경변수 `AS_DASHBOARD_BBS_ID`로 path에 고정한다.
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ----------------------- | ------ | ------------------------------------------ | ----------------------------- | ------------------------ | --------------------------------------------- |
|
||||
| 대시보드 공지 요약 목록 | GET | `/board-service/api/v1/posts/user/{bbsId}` | Query: `UserPostSearchParams` | `PageResponse<UserPost>` | `postApi.getUserPostList` / `useUserPostList` |
|
||||
|
||||
**Request (`UserPostSearchParams`)** — 본 화면 고정값
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------- | ---- | -------------- |
|
||||
| `page` | N | `0` |
|
||||
| `size` | N | `3` |
|
||||
| `keywordType` | N | `bbsTypeCd` |
|
||||
| `keyword` | N | `BBSNOTICE` |
|
||||
| `popupYn` 등 | N | 본 화면 미사용 |
|
||||
|
||||
**Response (`UserPost`) — 화면 매핑 필드**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ----------- | ------ | --------------------------- |
|
||||
| `rowNo` | number | 번호. 고정글이 아니면 표시 |
|
||||
| `pstNo` | number | 게시 번호. 상세 이동 키 |
|
||||
| `bbsId` | number | 게시판 ID |
|
||||
| `pstTtl` | string | 제목 |
|
||||
| `wrtrNm` | string | 작성자 |
|
||||
| `regDt` | string | 작성일 |
|
||||
| `inqNocs` | number | 조회수 |
|
||||
| `upndFixYn` | string | 상단고정 `Y`/`N` → `[공지]` |
|
||||
|
||||
---
|
||||
|
||||
## 9. UI 구현 가이드
|
||||
|
||||
### 9-1. 기술 스택·원칙
|
||||
|
||||
- 라우트: 기존 홈 `app/(main)/(with-base-layout)/page.tsx` → `pages/home` (현재 `MainPage` 플레이스홀더)
|
||||
- UI: `@common/components` — Card, Badge, Button, Table(공지). 지도 라이브러리 TBD
|
||||
- 데이터: React Query `useGetMainDashboard()`. 처리하기는 `useReadNoti()`. 공지는 `useUserPostList()`
|
||||
- 진행상황 카드: `docSeComCd`로 4슬롯 매핑. `TOTAL` 제외
|
||||
- 알림 목록: 테이블이 아닌 행 리스트(뱃지 + 일자 + 제목 + 버튼)
|
||||
- 공지: `@tanstack/react-table` + `useDataTable`, 페이지네이션 없음
|
||||
- 검색 조건 URL 동기화: 본 화면 필터 없음. `searchYear` UI가 생기면 그때 검토
|
||||
|
||||
### 9-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------- | ------------------------------------------------------------ |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/page.tsx` |
|
||||
| 페이지 | `pages/dashboard/ui/dashboard-page.tsx` |
|
||||
| 지도 | `pages/dashboard/ui/national-as-map.tsx` (API TBD, Skeleton) |
|
||||
| 진행상황 카드 | `pages/dashboard/ui/doc-prgrs-stts-cards.tsx` |
|
||||
| 알림이력 | `pages/dashboard/ui/dashboard-noti-list.tsx` |
|
||||
| 공지사항 | `pages/dashboard/ui/dashboard-notice-table.tsx` |
|
||||
| 공지 컬럼 | `pages/dashboard/model/dashboard-notice-table-columns.tsx` |
|
||||
| Entity API | `entities/dashboard/api/dashboard-service.ts` |
|
||||
| Entity Hook | `entities/dashboard/api/queries.ts` → `useGetMainDashboard` |
|
||||
| Entity Model | `entities/dashboard/model/dashboard-interface.ts` |
|
||||
| 알림 읽음 | `entities/noti` → `useReadNoti` |
|
||||
| 공지 Entity | `entities/board` → `useUserPostList` |
|
||||
|
||||
Entity(`getMainDashboard`, `getUserPostList`) 구현됨. 영역 B·C·D 화면 구현. 영역 A는 Skeleton.
|
||||
|
||||
### 9-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------------ | ---------------------------------------------------------------------------- |
|
||||
| A | `national-as-map` | Skeleton (지도 API TBD) |
|
||||
| B | `doc-prgrs-stts-cards` | `useGetMainDashboard` → `docPrgrsSttsList` (`TOTAL` 제외, 코드 매핑) |
|
||||
| C | `dashboard-noti-list` | 동 훅 → `notiStats`, `notiList`. 처리 → `useReadNoti`. 전체보기 `/nti-hstry` |
|
||||
| D | `dashboard-notice-table` | `useUserPostList` → `content[]`. [더보기]·행 클릭 → `/cms/bbsnotice/...` |
|
||||
|
||||
### 9-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | -------------------------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 |
|
||||
| 본 화면 라우트 | `/` (홈) |
|
||||
| 알림 처리 이동 | [처리하기] → `readNoti` 후 `notiUrl` (알림이력과 동일, SEWER URL은 상세 세그먼트 제거) |
|
||||
| 알림 전체보기 | `/nti-hstry` |
|
||||
| 공지 더보기 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}` (as 앱 공지 라우트 미구현, st/cm 관례) |
|
||||
| 공지 행 클릭 | `/cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}` |
|
||||
| 진행상황 카드 | §5-3 목록 라우트 (`/fnrs-cnsltn`, `/instl-sncn`, `/design-cnstn`, `/sewer-bsc-plan`) |
|
||||
|
||||
### 9-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입 (홈)
|
||||
CALL getMainDashboard({})
|
||||
DISPLAY 영역 B // docPrgrsSttsList — FNRS/INSTL/CNFM/SEWER 슬롯
|
||||
DISPLAY 영역 C // notiStats + notiList(≤5)
|
||||
|
||||
CALL getUserPostList({ page: 0, size: 3, keywordType: "bbsTypeCd", keyword: "BBSNOTICE" })
|
||||
DISPLAY 영역 D
|
||||
|
||||
CALL 지도 데이터 // TBD → 영역 A
|
||||
|
||||
ON 영역 C [전체보기] CLICK DO
|
||||
NAVIGATE /nti-hstry
|
||||
END ON
|
||||
|
||||
ON 영역 C [처리하기] CLICK DO
|
||||
CALL readNoti(notiId)
|
||||
IF notiUrl THEN
|
||||
NAVIGATE notiUrl
|
||||
END IF
|
||||
CALL getMainDashboard // invalidate dashboardBaseKey
|
||||
END ON
|
||||
|
||||
ON 영역 B 카드 CLICK DO
|
||||
NAVIGATE 문서 목록 // §5-3
|
||||
END ON
|
||||
|
||||
ON 영역 D [더보기] CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}
|
||||
END ON
|
||||
|
||||
ON 영역 D 행 CLICK DO
|
||||
NAVIGATE /cms/bbsnotice/{AS_DASHBOARD_BBS_ID}/{pstNo}
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -1,284 +0,0 @@
|
||||
# 화면설계서: 필수첨부파일관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | ---------------------------------------------- |
|
||||
| 버전 | 0.1 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 필수첨부파일관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 필수첨부파일관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
문서(업무) 구분별로 **필수 첨부파일** 항목을 등록·조회·수정·삭제한다. 상단 검색 영역에서 **문서구분**으로 목록을 필터링하고, **등록** 또는 행별 **수정**·**삭제**로 마스터 데이터를 유지한다. 등록·수정 시 **필수첨부파일아이디**는 클라이언트에서 입력(또는 생성)하여 요청 본문에 포함한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 필수첨부파일관리 브레드크럼: 디지털행정지원 > … > │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 / 액션 │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 문서구분 [전체 ▼] [조회] [등록] │ │
|
||||
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 필수첨부파일 목록 │
|
||||
│ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 필수첨부파일아이디 | 문서구분 | 필수첨부파일명 | 필수첨부여부 | 비고 | 등록일시 | 수정일시 | 관리 │
|
||||
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ EAF-0001 | 설치인가 | 사업계획서 | 예 | 서명본 첨부 | 2026-04-22 10:10:00 | — | [수정][삭제] │ │
|
||||
│ │ EAF-0002 | 기본계획 | 변경사유서 | 예 | — | 2026-04-22 11:00:00 | — | [수정][삭제] │ │
|
||||
│ └────────────────────────────────────────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
(모달) [다이얼로그 C] 필수첨부파일 등록 / 수정
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 필수첨부파일 등록 [닫기] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 필수첨부파일아이디(자동발급) [EAF-0004] │
|
||||
│ 문서구분 [설치인가 ▼] │
|
||||
│ 필수첨부파일명 [예: 사업계획서] │
|
||||
│ 필수첨부여부 [ ] 예 [ ] 아니오 │
|
||||
│ 비고 [비고를 입력하세요] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ [취소] [저장] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
CALL 필수첨부파일 목록 조회 (findPage)
|
||||
DISPLAY 필수첨부파일 그리드 (영역 B)
|
||||
|
||||
// 영역 A: 검색
|
||||
ON [조회] 버튼 CLICK DO
|
||||
CALL 필수첨부파일 목록 조회 (문서구분 = 선택값, findPage)
|
||||
DISPLAY 필수첨부파일 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
// 영역 A: 등록
|
||||
ON [등록] 버튼 CLICK DO
|
||||
OPEN 필수첨부파일 등록 다이얼로그 (다이얼로그 C)
|
||||
ON [저장] DO
|
||||
CONFIRM "등록하시겠습니까?"
|
||||
VALIDATE 문서구분, 필수첨부파일명, 필수첨부여부
|
||||
CALL 필수첨부파일 등록 (create)
|
||||
CALL 필수첨부파일 목록 조회
|
||||
CLOSE 다이얼로그
|
||||
TOAST "저장되었습니다."
|
||||
END ON
|
||||
ON [취소] / [닫기] DO
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B: 수정
|
||||
ON 행 [수정] CLICK DO
|
||||
OPEN 필수첨부파일 수정 다이얼로그 (기존 값 초기화)
|
||||
ON [저장] DO
|
||||
VALIDATE 문서구분, 필수첨부파일명, 필수첨부여부
|
||||
CALL 필수첨부파일 수정 (update)
|
||||
CALL 필수첨부파일 목록 조회
|
||||
CLOSE 다이얼로그
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B: 삭제
|
||||
ON 행 [삭제] CLICK DO
|
||||
CONFIRM "삭제하시겠습니까?"
|
||||
CALL 필수첨부파일 삭제 (remove)
|
||||
CALL 필수첨부파일 목록 조회
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A: 검색 / 액션
|
||||
|
||||
문서구분 조건으로 목록을 조회하고, 신규 등록 다이얼로그를 연다.
|
||||
|
||||
### 4-1. 검색 필드
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | ---------- | ---- | ------ | ----------------- | ------------------------------ |
|
||||
| 문서구분 | CodeSelect | N | 전체 | `docSeComCd?` | "전체" 선택 시 파라미터 미전송 |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ------ | -------------- | ----------------------------------------------- |
|
||||
| 조회 | 검색 필드 우측 | 현재 검색 조건으로 영역 B 목록 갱신 |
|
||||
| 등록 | 영역 우측 끝 | 필수첨부파일 등록 다이얼로그(다이얼로그 C) 오픈 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B: 필수첨부파일 목록
|
||||
|
||||
필수 첨부파일 마스터를 테이블로 표시한다. 페이지네이션 UI 적용.
|
||||
|
||||
### 5-1. 헤더
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ----------------- |
|
||||
| 섹션 타이틀 | 필수첨부파일 목록 |
|
||||
|
||||
### 5-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | ------------------- | ------ | ------------------------------------- |
|
||||
| 필수첨부파일아이디 | `esntlAtchFileId` | string | 예: `EAF-0001`, 클라이언트 생성/입력 |
|
||||
| 문서구분 | `docSeComCd` | string | 코드명 표시 — 공통코드 (TBD 상위코드) |
|
||||
| 필수첨부파일명 | `esntlAtchFileNm` | string | |
|
||||
| 필수첨부여부 | `esntlAtchYn` | string | `Y`→예, `N`→아니오 |
|
||||
| 비고 | `esntlAtchFileRmrk` | string | 없으면 `—` 표시 |
|
||||
| 등록일시 | `regDt` | string | `yyyy-MM-dd HH:mm:ss` |
|
||||
| 수정일시 | `mdfcnDt` | string | 미수정 시 빈 값 또는 `—` |
|
||||
| 관리 | — | — | 행별 **[수정] · [삭제]** |
|
||||
|
||||
- 행 클릭 시 별도 상세 화면 없음. 편집은 **[수정]** 버튼으로 다이얼로그 C를 연다.
|
||||
|
||||
### 5-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ------ | ---- | -------------------------------------- |
|
||||
| 수정 | 행별 | 필수첨부파일 수정 다이얼로그 (기존 값) |
|
||||
| 삭제 | 행별 | 확인 후 삭제 API 호출 → 목록 갱신 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 다이얼로그 C: 필수첨부파일 등록 / 수정
|
||||
|
||||
등록·수정 공용 폼 다이얼로그. 등록 시 제목 **「필수첨부파일 등록」**, 수정 시 **「필수첨부파일 수정」**.
|
||||
|
||||
### 6-1. 폼 필드
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ------------------ | ---------- | ---- | ------ | ------------------- | -------------------------------- |
|
||||
| 필수첨부파일아이디 | (Readonly) | Y | — | `esntlAtchFileId` | 등록 시 미노출 |
|
||||
| 문서구분 | CodeSelect | Y | — | `docSeComCd` | 예: 설치인가, 기본계획 |
|
||||
| 필수첨부파일명 | Text | Y | — | `esntlAtchFileNm` | placeholder: "예: 사업계획서" |
|
||||
| 필수첨부여부 | RadioGroup | Y | Y | `esntlAtchYn` | Y/N, UI 표시: 예/아니오 |
|
||||
| 비고 | Textarea | N | — | `esntlAtchFileRmrk` | placeholder: "비고를 입력하세요" |
|
||||
|
||||
### 6-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------- |
|
||||
| 저장 | POST(등록) 또는 PUT(수정) 후 목록 갱신 |
|
||||
| 취소 | 다이얼로그 닫기, 입력값 폐기 |
|
||||
| 닫기 | 다이얼로그 닫기 (헤더 우측) |
|
||||
|
||||
---
|
||||
|
||||
## 7. API/DTO 명세
|
||||
|
||||
### 7-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ----------------------------------------------------- |
|
||||
| 서비스 | AS Common Service |
|
||||
| Base prefix | `/as-service/api/v1/esntl-atch-file` |
|
||||
| 인증 | JWT (`Authorization`), `X-Site-Id` 등 |
|
||||
| 페이징 | Spring `Pageable` 사용 (`page`, `size`, `sort`) |
|
||||
| 엔티티 | `@/entities/esntl` (`esntl-service.ts`, `queries.ts`) |
|
||||
|
||||
### 7-2. 필수첨부파일 API
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API (클라이언트 함수명) |
|
||||
| --------- | ------ | -------------------- | ----------------------------- | ----------------------------------- | ----------------------- |
|
||||
| 트리 조회 | GET | `/list/tree` | Query: `docSeComCd`(필수) | `EsntlAtchFileMngTreeResponseDto[]` | `findTree` |
|
||||
| 목록 조회 | GET | `/` | Query: Search + Pageable | `PageResponse<EsntlAtchFileMng>` | `findPage` |
|
||||
| 단건 조회 | GET | `/{esntlAtchFileId}` | Path: `esntlAtchFileId` | `EsntlAtchFileMng` | `findById` |
|
||||
| 등록 | POST | `/` | `EsntlAtchFileMngSaveRequest` | `string` (생성된 ID) | `create` |
|
||||
| 수정 | PUT | `/{esntlAtchFileId}` | `EsntlAtchFileMngSaveRequest` | `string` (수정된 ID) | `update` |
|
||||
| 삭제 | DELETE | `/{esntlAtchFileId}` | Path: `esntlAtchFileId` | `string` (삭제된 ID) | `remove` |
|
||||
|
||||
**응답 DTO**
|
||||
|
||||
| Field | 타입 | 설명 |
|
||||
| ------------------- | ------- | -------------------------------- |
|
||||
| `esntlAtchFileId` | string | 필수첨부파일아이디 |
|
||||
| `docSeComCd` | string | 문서구분공통코드 |
|
||||
| `esntlAtchFileNm` | string | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | string | 필수첨부여부 (Y/N) |
|
||||
| `esntlAtchFileRmrk` | string? | 필수첨부파일비고 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
| `rgtrNm` | string? | 등록자명 |
|
||||
| `mdfr` | string? | 수정자 UUID |
|
||||
| `regDt` | string? | 등록일시 (`yyyy-MM-dd HH:mm:ss`) |
|
||||
| `mdfcnDt` | string? | 수정일시 |
|
||||
|
||||
**저장 Request**
|
||||
|
||||
| Field | 필수 | 설명 |
|
||||
| ------------------- | ---- | ------------------ |
|
||||
| `esntlAtchFileId` | Y | 필수첨부파일아이디 |
|
||||
| `docSeComCd` | N | 문서구분 |
|
||||
| `esntlAtchFileNm` | N | 필수첨부파일명 |
|
||||
| `esntlAtchYn` | N | 필수첨부여부 |
|
||||
| `esntlAtchFileRmrk` | N | 비고 |
|
||||
|
||||
**검색 Query**
|
||||
|
||||
| Field | 필수 | 설명 |
|
||||
| ----------------- | ---- | ----------------------------- |
|
||||
| `docSeComCd` | N | 문서구분 (전체 시 미전송) |
|
||||
| `esntlAtchFileNm` | N | 필수첨부파일명 부분/일치 검색 |
|
||||
| `esntlAtchYn` | N | 필수첨부여부 (`Y`/`N`) |
|
||||
| `page` | N | 페이지 번호 (0-base) |
|
||||
| `size` | N | 페이지 크기 (기본값: 100) |
|
||||
| `sort` | N | 정렬 조건 (예: `regDt,desc`) |
|
||||
|
||||
### 7-3. 공통코드
|
||||
|
||||
| UI 필드 | 코드 그룹 | 예시 값 |
|
||||
| ------------ | --------- | ------------------ |
|
||||
| 문서구분 | ASDOCSECD | 설치인가, 기본계획 |
|
||||
| 필수첨부여부 | 고정 | Y(예), N(아니오) |
|
||||
|
||||
---
|
||||
|
||||
## 8. UI 구현 가이드
|
||||
|
||||
### 8-1. 기술 스택·원칙
|
||||
|
||||
- **공통 컴포넌트**: `@common/components` (`Dialog`, `Button`, `Input`, `Select`, `Textarea`, `Label` 등)
|
||||
- **테이블**: `@tanstack/react-table` + `useDataTable`
|
||||
- **폼**: `react-hook-form`
|
||||
- **데이터**: React Query — `@/entities/esntl/api/queries.ts` 훅 사용
|
||||
- **공통코드**: `@/entities/code` — `CodeSelect`, `findCodeLabel`
|
||||
- **확인**: `@frontend-opensource/use-react-hooks` — `useConfirm` (등록·삭제 확인)
|
||||
|
||||
### 8-2. 모듈·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/esntl-atch-file-mng/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/esntl-atch-file-mng/` (Public API: page만 export) |
|
||||
| UI | `.../ui/esntl-atch-file-mng-page.tsx`, `esntl-atch-file-list.tsx`, `esntl-atch-file-form-dialog.tsx` |
|
||||
| model | `.../model/esntl-atch-file-columns.tsx`, `esntl-atch-file-search-params.ts` |
|
||||
| 엔티티 | `apps/as/src/entities/esntl/` (`esntl-service.ts`, `queries.ts`, `esntl-interface.ts`) |
|
||||
| 문서 | `apps/as/docs/essential-attachment-file-management/SPEC.md` |
|
||||
|
||||
### 8-3. 데이터 흐름(요약)
|
||||
|
||||
- **진입**: 검색 params 기본값(`docSeComCd` = 전체) → 목록 query → 영역 B DataTable.
|
||||
- **조회**: [조회] 클릭 시 search state 반영 → 목록 query refetch.
|
||||
- **등록**: [등록] → Dialog `mode=create` → [저장] → POST → 목록 invalidate.
|
||||
- **수정**: [수정] → Dialog `mode=edit` + row 초기값 → [저장] → PUT → 목록 invalidate.
|
||||
- **삭제**: confirm → DELETE → 목록 invalidate.
|
||||
@@ -1,285 +0,0 @@
|
||||
# 화면설계서: 내 행정지원
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 1.0 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 내 행정지원 |
|
||||
| 화면 ID | AS_SC0204011 |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 > 행정지원서비스 > 내 행정지원 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인한 사용자가 **본인이 신청한 행정지원 문서**를 기간·구분·진행상태로 조회한다. 목록에서 행을 선택하면 해당 문서 상세 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 브레드크럼: 메인 > 행정지원서비스 > 내 행정지원 │
|
||||
│ 페이지 타이틀: 내 행정지원 │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 기간 [시작일] ~ [종료일] 구분 ▼ 진행상태 ▼ [검색] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 내 신청 목록 │
|
||||
│ 타이틀: 내 신청 목록 N개 │
|
||||
│ ┌────┬──────┬────────┬──────────────┬────────────┬────────┬──────────────┐ │
|
||||
│ │ No │ 구분 │ 신청일 │ 진행상태 │ 완료(예정)일│ 담당자 │ 담당자연락처 │ │
|
||||
│ ├────┼──────┼────────┼──────────────┼────────────┼────────┼──────────────┤ │
|
||||
│ │ … │ … │ … │ prgrsSttsComNm│ … │ … │ … │ │
|
||||
│ └────┴──────┴────────┴──────────────┴────────────┴────────┴──────────────┘ │
|
||||
│ [페이지네이션] │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// 기간 기본값: 오늘 기준 최근 1개월 (startDate = today-1M, endDate = today)
|
||||
CALL findMyElaperDocList(ElaprMyDocSearchDto + pageable)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
|
||||
ON [검색] 버튼 CLICK DO
|
||||
CALL findMyElaperDocList(영역 A 조건 + page=0)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 페이지네이션 CHANGE DO
|
||||
CALL findMyElaperDocList(영역 A 조건 + page/size)
|
||||
DISPLAY 내 신청 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 영역 B 행 CLICK DO
|
||||
// docSeComCd·prgrsSttsComCd·rgtr → view/edit
|
||||
NAVIGATE 문서 상세 또는 수정 화면
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **기간 · 구분 · 진행상태** (+ 검색).
|
||||
|
||||
> UI설계 상세설명의 `docSecs` / `docStatusCd`는 레거시·설계 문서 명칭이다.
|
||||
> **API SoT**는 `ElaprMyDocSearchDto`의 `docSeComCd` / `prgrsSttsComCd`를 사용한다.
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | ---------------- | ---- | --------------------------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDate` | 신청일(`reqDt`) 기준 검색으로 가정 |
|
||||
| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDate` | |
|
||||
| 구분 | Select(공통코드) | N | 전체(미선택) | `docSeComCd` | 상위코드 `ASDOCSE`. UI설계 `docSecs` → API `docSeComCd` |
|
||||
| 진행상태 | Select(공통코드) | N | 전체(미선택) | `prgrsSttsComCd` | 상위코드 = `{docSeComCd}STTS` (예: `INSTL` → `INSTLSTTS`). UI설계 `docStatusCd` → API 필드명 |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | ----------------------------------------------------------------- |
|
||||
| 검색 | 영역 A 조건으로 `findMyElaperDocList` 호출 → 영역 B 갱신 (page=0) |
|
||||
|
||||
### 4-3. 콤보 의존
|
||||
|
||||
| 트리거 | 동작 |
|
||||
| ----------- | ------------------------------------------------------------------- |
|
||||
| 구분 변경 | 진행상태 옵션을 `upCd = docSeComCd + "STTS"`로 재조회. 진행상태 선택값 RESET |
|
||||
| 구분 미선택 | 진행상태 비활성(옵션 비움). `upCd` 조합 불가이므로 조회하지 않음 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 내 신청 목록
|
||||
|
||||
### 5-1. 그리드 컬럼
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------------ | ---------------- | ------------------------------------------------- |
|
||||
| No | (row index) | 페이지 오프셋 반영 |
|
||||
| 구분 | `docSeComNm` | 코드 `docSeComCd` (`ASDOCSE`) |
|
||||
| 신청일 | `reqDt` | 표시 `YYYY-MM-DD` (응답은 `yyyy-MM-dd HH:mm:ss`) |
|
||||
| 진행상태 | `prgrsSttsComNm` | 텍스트 렌더링 (단계형 프로그레스 미사용) |
|
||||
| 완료(예정)일 | `cmptnExpctDt` | 표시 `YYYY-MM-DD` |
|
||||
| 담당자 | `picNm` | |
|
||||
| 담당자연락처 | `picTelno` | |
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| `docNo` | 문서번호 — 행 클릭 라우팅 키 |
|
||||
| `docTtl` | 문서명/사업명 — 미표시 |
|
||||
| `chgCycl` | 변경차수 — 그리드 미표시. SEWER 이동 시 query 파라미터로 사용 |
|
||||
| `prgrsSttsComCd` | 진행상태 코드 — 컬럼 표시는 `prgrsSttsComNm` |
|
||||
| `rgtr` | 등록자 UUID — edit 판정용 (`rgtr === userUnqId`) |
|
||||
|
||||
### 5-2. 목록 메타·페이지네이션
|
||||
|
||||
| 항목 | 매핑 |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| 건수 `N개` | `PageResponse.totalElements` |
|
||||
| page / size | `ElaprMyDocSearchDto`의 `PageableParams` |
|
||||
| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
|
||||
|
||||
### 5-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| ------- | -------------------------------------------------------------------------------------------- |
|
||||
| 행 클릭 | `docSeComCd`·`prgrsSttsComCd`·`rgtr`로 view/edit 분기 후 이동 (`resolveMyPbadmsSprtDetailPath`) |
|
||||
|
||||
**edit 조건** (기존 도메인 목록과 동일):
|
||||
|
||||
| `docSeComCd` | edit 조건 |
|
||||
| ------------------------ | ---------------------------------------------------------------------- |
|
||||
| `INSTL` / `FNRS` / `SEWER` | `(prgrsSttsComCd === "SV" \|\| "SL") && rgtr === loggedInUserUnqId` |
|
||||
| `CNFM` | `prgrsSttsComCd === "CV" && rgtr === loggedInUserUnqId` |
|
||||
|
||||
| `docSeComCd` | edit 경로 | view 경로 |
|
||||
| ------------ | -------------------------------------------- | --------------------------------------- |
|
||||
| `FNRS` | `/fnrs-cnsltn/{docNo}/edit` | `/fnrs-cnsltn/{docNo}` |
|
||||
| `INSTL` | `/instl-sncn/{docNo}/edit` | `/instl-sncn/{docNo}` |
|
||||
| `CNFM` | `/design-cnstn/{docNo}/edit` | `/design-cnstn/{docNo}` |
|
||||
| `SEWER` | `/sewer-bsc-plan/{docNo}/edit?chgCycl={n}` | `/sewer-bsc-plan/{docNo}?chgCycl={n}` |
|
||||
| 그 외 | 이동하지 않음 | |
|
||||
|
||||
- SEWER `chgCycl`: `row.chgCycl ?? 1`
|
||||
- 조건 불충족 시 항상 view
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Base prefix | `/as-elapr-service/api/v1/elapr-prgrs-stts` (`ENDPOINT.AS_ELAPR_SERVICE.ELAPR_PRGRS_STTS`) |
|
||||
| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
|
||||
| 클라이언트 모듈 | `@/entities/elapr` — `elaprPrgrsSttsService` / `useElaprPrgrsSttsMyDocPage` |
|
||||
|
||||
### 6-2. 내 행정지원 목록 — `findMyElaperDocList`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ----------------- | ------ | --------------- | --------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| 내 신청 목록 조회 | GET | `{BASE}/my/doc` | Query: `ElaprMyDocSearchDto` + Pageable | `PageResponse<MyAdssDocResponseDto>` | `elaprPrgrsSttsService.findMyElaperDocList` / `useElaprPrgrsSttsMyDocPage` |
|
||||
|
||||
**Request (`ElaprMyDocSearchDto` + Pageable)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------------------ | ---- | ------------------------------------------------------------ |
|
||||
| `startDate` | N | 검색 시작일 `YYYY-MM-DD` |
|
||||
| `endDate` | N | 검색 종료일 `YYYY-MM-DD` |
|
||||
| `docSeComCd` | N | 문서구분 (`INSTL` / `FNRS` / `DESIGN` / `SEWER` / `PWRK` 등) |
|
||||
| `prgrsSttsComCd` | N | 문서진행상태공통코드 |
|
||||
| `page` / `size` / `sort` | N | Spring Pageable |
|
||||
|
||||
**Response (`MyAdssDocResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ---------------- | ------- | ------------------------------------------- |
|
||||
| `docNo` | string | 문서번호 |
|
||||
| `docTtl` | string? | 문서명/사업명 |
|
||||
| `docSeComCd` | string? | 문서구분 코드 |
|
||||
| `docSeComNm` | string? | 문서구분 코드명 (`ASDOCSE`) |
|
||||
| `reqDt` | string? | 신청일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `prgrsSttsComCd` | string? | 문서진행상태 코드 |
|
||||
| `prgrsSttsComNm` | string? | 문서진행상태 코드명 |
|
||||
| `cmptnExpctDt` | string? | 완료(예정)일 `yyyy-MM-dd` |
|
||||
| `picNm` | string? | 담당자 성명 |
|
||||
| `picTelno` | string? | 담당자 연락처 |
|
||||
| `chgCycl` | number? | 변경차수 |
|
||||
| `rgtr` | string? | 등록자 UUID |
|
||||
|
||||
### 6-3. 공통코드 (콤보)
|
||||
|
||||
| 용도 | 상위코드 | API | 비고 |
|
||||
| -------- | ------------ | --------------------------- | ----------------------------- |
|
||||
| 구분 | `ASDOCSE` | `useCodeList("ASDOCSE")` 등 | 기존 관리 화면과 동일 |
|
||||
| 진행상태 | `{docSeComCd}STTS` | `useCodeList(docSeComCd + "STTS")` | 예: `INSTLSTTS`, `FNRSSTTS`, `SEWERSTTS`. 구분 미선택 시 미호출 |
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — DatePicker, Select/`CodeSelect`, Button, DataTable, Pagination
|
||||
- 목록: `@tanstack/react-table` + `useDataTable` (기존 `instl-list` / `fnrs-list` 패턴)
|
||||
- 데이터: React Query `useElaprPrgrsSttsMyDocPage(params)`
|
||||
- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
|
||||
- 진행상태 컬럼: `prgrsSttsComNm` 텍스트 렌더링 (단계형 프로그레스 미사용)
|
||||
- 행 클릭: `docSeComCd` + `prgrsSttsComCd` + `rgtr`로 view/edit 분기 (`SEWER`는 `chgCycl` query 포함)
|
||||
|
||||
### 7-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | -------------------------------------------------------------- |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/my-pbadms-sprt/page.tsx` |
|
||||
| 페이지 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list-page.tsx` |
|
||||
| 목록 그리드 | `pages/my-pbadms-sprt/ui/my-pbadms-sprt-list.tsx` |
|
||||
| 경로 해석 | `pages/my-pbadms-sprt/lib/resolve-detail-path.ts` |
|
||||
| 테이블 컬럼 | `pages/my-pbadms-sprt/model/my-pbadms-sprt-table-columns.tsx` |
|
||||
| Entity API | `entities/elapr/api/elapr-prgrs-stts-service.ts` |
|
||||
| Entity Hook | `entities/elapr/api/queries.ts` → `useElaprPrgrsSttsMyDocPage` |
|
||||
| Entity Model | `entities/elapr/model/elapr-prgrs-stts-interface.ts` |
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | -------------------------- | ------------------------------------------ |
|
||||
| A | `my-pbadms-sprt-list-page` | 로컬 state → `ElaprMyDocSearchDto` |
|
||||
| B | DataTable + columns | `useElaprPrgrsSttsMyDocPage` → `content[]` |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 (목록 전용) |
|
||||
| 목록 라우트 | `/my-pbadms-sprt` |
|
||||
| 상세 이동 | §5-3 표와 동일 (view/edit + `rgtr` 소유자 검사, SEWER `chgCycl`) |
|
||||
|
||||
### 7-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
SET 기간 = 최근 1개월
|
||||
CALL findMyElaperDocList
|
||||
DISPLAY 영역 B
|
||||
|
||||
ON [검색] CLICK DO
|
||||
CALL findMyElaperDocList // → 영역 B 갱신
|
||||
END ON
|
||||
|
||||
ON 페이지 CHANGE DO
|
||||
CALL findMyElaperDocList
|
||||
END ON
|
||||
|
||||
ON 영역 B 행 CLICK DO
|
||||
IF NOT docNo OR unsupported docSeComCd THEN
|
||||
// no-op
|
||||
ELSE IF canEdit(prgrsSttsComCd, rgtr === userUnqId) THEN
|
||||
NAVIGATE edit 경로 // SEWER면 ?chgCycl=
|
||||
ELSE
|
||||
NAVIGATE view 경로
|
||||
END IF
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# 화면설계서: 알림이력
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | -------------------------------------------- |
|
||||
| 버전 | 1.4 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 알림이력 |
|
||||
| 화면 ID | AS_SC0205011 |
|
||||
| 요구사항 ID | NSUR-001-001 |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 메인 > 행정지원서비스 > 알림이력 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
로그인한 사용자가 **본인에게 발송된 행정지원(ADSS) 알림**을 기간·구분으로 조회한다. 목록의 처리 버튼으로 해당 알림을 읽음 처리한 뒤, 연계된 업무 화면으로 이동한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 브레드크럼: 메인 > 행정지원서비스 > 알림이력 │
|
||||
│ 페이지 타이틀: 알림이력 │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 기간 [시작일] ~ [종료일] 구분 ▼ [검색] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 알림 이력 목록 │
|
||||
│ 타이틀: 알림 이력 목록 N개 ▼ │
|
||||
│ ┌────┬──────────────┬────────────────┬──────────┬────────┬────────┐ │
|
||||
│ │ No │ 알림일시 │ 알림내용 │ 연계업무 │ 상태 │ 처리 │ │
|
||||
│ ├────┼──────────────┼────────────────┼──────────┼────────┼────────┤ │
|
||||
│ │ … │ regDt │ notiTtl │ docSeComNm│ Badge │ 버튼 │ │
|
||||
│ └────┴──────────────┴────────────────┴──────────┴────────┴────────┘ │
|
||||
│ [페이지네이션] │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
// 기간 기본값: 오늘 기준 최근 1개월 (시작일 = today-1M, 종료일 = today)
|
||||
CALL findMyAsNotiList(NotiSearchDto + pageable)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
|
||||
ON [검색] 버튼 CLICK DO
|
||||
CALL findMyAsNotiList(영역 A 조건 + page=0)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 페이지네이션 CHANGE DO
|
||||
CALL findMyAsNotiList(영역 A 조건 + page/size)
|
||||
DISPLAY 알림 이력 목록 그리드 (영역 B)
|
||||
END ON
|
||||
|
||||
ON 영역 B [처리] 버튼 CLICK DO
|
||||
CALL readNoti(notiId)
|
||||
// UI설계: 해당 알림에 연계된 URL로 이동
|
||||
NAVIGATE notiUrl
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **기간 · 구분** (+ 검색).
|
||||
|
||||
서버 `findMyAsNotiList` 고정 정책 (클라이언트 미전달):
|
||||
|
||||
- `recvUnqId` = 로그인 사용자 UUID
|
||||
- `delYn` = `N`
|
||||
- `notiType` = `ADSS` (행정지원 전용 API)
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| ---------- | ---------- | ---- | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 기간(시작) | DatePicker | N | 오늘 − 1개월 (`YYYY-MM-DD`) | `startDt` | `NotiSearchDto.startDt` |
|
||||
| 기간(종료) | DatePicker | N | 오늘 (`YYYY-MM-DD`) | `endDt` | `NotiSearchDto.endDt` |
|
||||
| 구분 | Select | N | 전체(미선택) | `docSeComCd` | `INSTL` / `FNRS` / `SEWER` / `CNFM` / `PWRK` (또는 단축 `I`/`F`/`S`/`C`/`P`). 공통코드 `ASDOCSE` |
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------------------------------- |
|
||||
| 검색 | 영역 A 조건으로 `findMyAsNotiList` 호출 → 영역 B 갱신 (page=0) |
|
||||
|
||||
### 4-3. 콤보·기본값 주의
|
||||
|
||||
| 항목 | 내용 |
|
||||
| -------------- | ---------------------------------------------------------- |
|
||||
| 구분 = 전체 | `docSeComCd` 미전달. `notiType`은 서버가 `ADSS`로 고정 |
|
||||
| 구분 옵션 소스 | 공통코드 `ASDOCSE` (`docSeComCd` ↔ `docSeComNm`) |
|
||||
| 기간 | UI설계 상세설명: 기본 최근 1개월. `startDt`/`endDt`로 전달 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 알림 이력 목록
|
||||
|
||||
### 5-1. 그리드 컬럼
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| -------- | ------------ | --------------------------------------------------------------- |
|
||||
| No | (row index) | 페이지 오프셋 반영 |
|
||||
| 알림일시 | `regDt` | 표시 `YYYY-MM-DD HH:mm` (응답 `yyyy-MM-dd HH:mm:ss`) |
|
||||
| 알림내용 | `notiTtl` | 제목 문자열. UI 샘플은 `[YYYY-MM-DD] …` 접두 |
|
||||
| 연계업무 | `docSeComNm` | 문서구분 공통코드명 (`docSeComCd` + `@CodeName` `ASDOCSE`) |
|
||||
| 상태 | (derived) | `Badge` 렌더링. 판정 규칙은 §5-1-1 |
|
||||
| 처리 | (action) | 상태별 버튼 라벨. 클릭 시 `readNoti` 후 `notiUrl`로 이동 (§5-3) |
|
||||
|
||||
#### 5-1-1. 상태 UI 케이스 (`@common/components` `Badge`)
|
||||
|
||||
판정에 사용: `notiPrcsCmptnYn`, `notReadYn`, `regDt` (오늘 기준 경과일).
|
||||
|
||||
| 조건 | 라벨 | Badge |
|
||||
| ------------------------------------------------------------------------------ | -------- | ----------------------------- |
|
||||
| `notiPrcsCmptnYn` = `Y` | 처리완료 | `variant="default"` (primary) |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 초과** | 처리지연 | `color="red"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `Y` 이고 `regDt`로부터 **7일 이내** | 처리중 | `color="green"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 초과** | 지연 | `color="orange"` |
|
||||
| `notiPrcsCmptnYn` = `N` 이고 `notReadYn` = `N` 이고 `regDt`로부터 **3일 이내** | 미조회 | `color="yellow"` |
|
||||
|
||||
```
|
||||
IF notiPrcsCmptnYn = Y THEN
|
||||
DISPLAY Badge "처리완료" (primary / variant=default)
|
||||
ELSE IF notReadYn = Y THEN
|
||||
IF daysSince(regDt) > 7 THEN
|
||||
DISPLAY Badge "처리지연" (red)
|
||||
ELSE
|
||||
DISPLAY Badge "처리중" (green)
|
||||
END IF
|
||||
ELSE // notReadYn = N
|
||||
IF daysSince(regDt) > 3 THEN
|
||||
DISPLAY Badge "지연" (orange)
|
||||
ELSE
|
||||
DISPLAY Badge "미조회" (yellow)
|
||||
END IF
|
||||
END IF
|
||||
```
|
||||
|
||||
- `daysSince(regDt)`: 오늘 날짜 − `regDt` 일자 (일 단위)
|
||||
- `regDt`가 없으면 상태 Badge 표시 전략 TBD (미표시 또는 기본값)
|
||||
|
||||
응답에 있으나 그리드에 **표시하지 않음**:
|
||||
|
||||
| Field | 비고 |
|
||||
| ----------------- | ---------------------------------------------------------------------- |
|
||||
| `notiId` | 알림 ID — 행 키 |
|
||||
| `recvUnqId` | 수신 회원 UUID (서버가 로그인 사용자로 고정) |
|
||||
| `senderUnqId` | 발신 회원 UUID |
|
||||
| `notiType` | 알림 종류 (`ADSS`) |
|
||||
| `notiCn` | 알림 내용 (목록에서는 서버가 제외할 수 있음) |
|
||||
| `notReadYn` | 확인 여부 `Y`/`N`. 상태 Badge 판정에 사용 (§5-1-1) |
|
||||
| `notiUrl` | 처리 버튼 이동 URL. 컬럼 미표시 |
|
||||
| `notiLnkgUnqNo` | 알림연결고유번호 |
|
||||
| `docSeComCd` | 문서구분 공통코드. 연계업무 표시는 `docSeComNm` 사용 |
|
||||
| `notiIdntyDt` | 알림 확인 일시 |
|
||||
| `notiIdntyYn` | 알림 확인 여부 (`notReadYn`과 동일 의미). 상태 판정은 `notReadYn` 사용 |
|
||||
| `notiPrcsCmptnYn` | 알림 처리 완료 여부. 상태 Badge 판정에 사용 (§5-1-1) |
|
||||
| `notiPrcsCmptnDt` | 알림 처리 완료 일시 |
|
||||
|
||||
### 5-2. 목록 메타·페이지네이션
|
||||
|
||||
| 항목 | 매핑 |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| 건수 / size ▼ | 상단 우측 `N개` 셀렉트 → `Pageable.size` |
|
||||
| page / size | `NotiSearchDto`의 `PageableParams` |
|
||||
| 페이지 컨트롤 | Spring `Page` 표준 (`number`, `totalPages`) |
|
||||
|
||||
### 5-3. 행 상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| -------------- | --------------------------------------------------------------------------- |
|
||||
| 처리 버튼 클릭 | `readNoti(notiId)`로 읽음 처리 후, 해당 알림에 연계된 URL(`notiUrl`)로 이동 |
|
||||
| 행 클릭 | UI설계에 없음. 처리 버튼만 명시 |
|
||||
|
||||
**처리 버튼 라벨** (상태 Badge §5-1-1 기준):
|
||||
|
||||
| 상태 | 버튼명 |
|
||||
| -------- | -------- |
|
||||
| 미조회 | 처리하기 |
|
||||
| 지연 | 처리하기 |
|
||||
| 처리중 | 처리하기 |
|
||||
| 처리지연 | 처리하기 |
|
||||
| 처리완료 | 상세보기 |
|
||||
|
||||
- `notiUrl`이 비어 있으면 이동하지 않음 (no-op). 읽음 처리는 `notiUrl` 유무와 관계없이 호출
|
||||
- 읽음 처리 성공 후 목록 쿼리(`notiBaseKey`)를 invalidate 하여 영역 B 상태를 갱신
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Base prefix | `/cms-service/api/v1/noti` (`ENDPOINT.CMS_SERVICE.NOTI`) |
|
||||
| 인증 | 로그인 사용자 UUID — 서버 `SecurityUtils.getLoggedInUserUnqIdUUID()` (클라이언트 미전달) |
|
||||
| 클라이언트 모듈 | `@/entities/noti` — `notiService` / `useFindMyAsNotiList` / `useReadNoti` |
|
||||
|
||||
### 6-2. 로그인한 사용자 행정지원 알림 목록 — `findMyAsNotiList`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ---------------------------------- | ------ | ------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------ |
|
||||
| 로그인한 사용자 행정지원 알림 목록 | GET | `{BASE}/as/my/list` | Query: `NotiSearchDto` + Pageable | `PageResponse<NotiAsUserResponseDto>` | `notiService.findMyAsNotiList` / `useFindMyAsNotiList` |
|
||||
|
||||
**Request (`NotiSearchDto` + Pageable)**
|
||||
|
||||
| 필드 | 필수 | 설명 |
|
||||
| ------------------------ | ---- | --------------------------------------------------------------------------- |
|
||||
| `recvUnqId` | N | 수신 회원 UUID. 서버가 로그인 사용자로 덮어씀 |
|
||||
| `senderUnqId` | N | 발신 회원 UUID. 본 화면 필터에 없음 |
|
||||
| `notiType` | N | 알림 종류. 본 API는 서버가 `ADSS`로 고정 |
|
||||
| `notiTtlKeyword` | N | 알림 제목 키워드. 본 화면 필터에 없음 |
|
||||
| `notiCnKeyword` | N | 알림 내용 키워드. 본 화면 필터에 없음 |
|
||||
| `notReadYn` | N | 알림 확인 여부 (`Y`/`N`). 본 화면 검색 필터에 없음 |
|
||||
| `delYn` | N | 삭제 여부. 서버가 `N`으로 고정 |
|
||||
| `startDt` | N | 시작일자 `YYYY-MM-DD` — 영역 A 기간(시작) |
|
||||
| `endDt` | N | 종료일자 `YYYY-MM-DD` — 영역 A 기간(종료) |
|
||||
| `docSeComCd` | N | 문서 구분 공통 코드 (`INSTL`/`FNRS`/`SEWER`/`CNFM`/`PWRK` 등) — 영역 A 구분 |
|
||||
| `page` / `size` / `sort` | N | Spring Pageable |
|
||||
|
||||
**Response (`NotiAsUserResponseDto`)**
|
||||
|
||||
| 필드 | 타입 | 설명 |
|
||||
| ----------------- | ------- | ------------------------------------------------------- |
|
||||
| `notiId` | number | 알림 ID |
|
||||
| `recvUnqId` | string | 수신 회원 UUID |
|
||||
| `senderUnqId` | string | 발신 회원 UUID |
|
||||
| `notiType` | string | 알림 종류 (`ADSS`) |
|
||||
| `notiTtl` | string | 알림 제목 |
|
||||
| `notiCn` | string? | 알림 내용 (목록에서 제외될 수 있음) |
|
||||
| `notReadYn` | string? | 알림 확인 여부 (`Y`/`N`). 상태 Badge 판정 |
|
||||
| `regDt` | string? | 등록 일시 `yyyy-MM-dd HH:mm:ss`. 상태 Badge 경과일 기준 |
|
||||
| `notiUrl` | string? | 알림 URL 주소 |
|
||||
| `notiLnkgUnqNo` | string? | 알림연결고유번호 |
|
||||
| `docSeComCd` | string? | 문서구분 공통코드 |
|
||||
| `docSeComNm` | string? | 문서구분 공통코드명 (`ASDOCSE`) |
|
||||
| `notiIdntyDt` | string? | 알림 확인 일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
| `notiIdntyYn` | string? | 알림 확인 여부 (`Y`/`N`) |
|
||||
| `notiPrcsCmptnYn` | string? | 알림 처리 완료 여부 (`Y`/`N`). 상태 Badge 판정 |
|
||||
| `notiPrcsCmptnDt` | string? | 알림 처리 완료 일시 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
### 6-3. 알림 읽음 처리 — `readNoti`
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------------- | ------ | ---------------------- | ----------------------- | ---------------- | -------------------------------------- |
|
||||
| 알림 읽음 처리 | PUT | `{BASE}/{notiId}/read` | Path: `notiId` (`Long`) | `Long` (알림 ID) | `notiService.readNoti` / `useReadNoti` |
|
||||
|
||||
**Request**
|
||||
|
||||
| 필드 | 위치 | 필수 | 설명 |
|
||||
| -------- | ---- | ---- | ------- |
|
||||
| `notiId` | Path | Y | 알림 ID |
|
||||
|
||||
Body 없음.
|
||||
|
||||
**Response**
|
||||
|
||||
| 타입 | 설명 |
|
||||
| ------ | ------------------------------ |
|
||||
| number | 읽음 처리된 알림 ID (`notiId`) |
|
||||
|
||||
화면 매핑: 영역 B [처리] 버튼 클릭 시 호출. 성공 시 `notiBaseKey` invalidate → `findMyAsNotiList` 재조회.
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — DatePicker, Select, Button, DataTable, Pagination, **Badge**
|
||||
- 목록: `@tanstack/react-table` + `useDataTable` (기존 `my-pbadms-sprt-list` 패턴)
|
||||
- 데이터: React Query `useFindMyAsNotiList(params)`, `useReadNoti()`
|
||||
- 검색 조건·페이지: URL `searchParams` 동기화 권장 (`useSearchParamsState` 스킬)
|
||||
- 처리 컬럼: 행 클릭이 아니라 **버튼 클릭**으로 `readNoti` 후 `notiUrl` 이동
|
||||
- 상태 컬럼: §5-1-1 규칙으로 `Badge` 렌더링 (`notiPrcsCmptnYn` / `notReadYn` / `regDt`)
|
||||
- 연계업무: `docSeComNm` 표시
|
||||
|
||||
### 7-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | --------------------------------------------------------------------- |
|
||||
| 라우트 | `app/(main)/(with-base-layout)/noti/page.tsx` |
|
||||
| 페이지 | `pages/noti/ui/noti-list-page.tsx` |
|
||||
| 목록 그리드 | `pages/noti/ui/noti-list.tsx` |
|
||||
| 테이블 컬럼 | `pages/noti/model/noti-table-columns.tsx` |
|
||||
| 상태 판정 | `pages/noti/lib/resolve-noti-status.ts` |
|
||||
| Entity API | `entities/noti/api/noti-service.ts` |
|
||||
| Entity Hook | `entities/noti/api/queries.ts` → `useFindMyAsNotiList`, `useReadNoti` |
|
||||
| Entity Model | `entities/noti/model/noti-interface.ts` |
|
||||
|
||||
화면 레이어(`pages/noti`)·Entity(`findMyAsNotiList`, `readNoti`) 구현 완료.
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------- | -------------------------------------------------------------- |
|
||||
| A | `noti-list-page` | 로컬 state → `NotiSearchDto` |
|
||||
| B | DataTable + columns | `useFindMyAsNotiList` → `content[]`. 처리 버튼 → `useReadNoti` |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | --------------------------------------------------------------------- |
|
||||
| 다이얼로그 | 없음 (목록 전용) |
|
||||
| 목록 라우트 | `/noti` |
|
||||
| 상세 이동 | 처리 버튼 → `readNoti(notiId)` 후 `notiUrl` (절대/상대 경로 형식 TBD) |
|
||||
|
||||
### 7-5. 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
SET 기간 = 최근 1개월 // startDt / endDt 전달
|
||||
CALL findMyAsNotiList
|
||||
DISPLAY 영역 B
|
||||
|
||||
ON [검색] CLICK DO
|
||||
CALL findMyAsNotiList // → 영역 B 갱신
|
||||
END ON
|
||||
|
||||
ON 페이지 CHANGE DO
|
||||
CALL findMyAsNotiList
|
||||
END ON
|
||||
|
||||
ON 영역 B [처리] 버튼 CLICK DO
|
||||
CALL readNoti(notiId) // → 영역 B 상태 갱신 (invalidate)
|
||||
IF NOT notiUrl THEN
|
||||
// no-op (읽음 처리는 수행됨)
|
||||
ELSE
|
||||
NAVIGATE notiUrl
|
||||
END IF
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
@@ -1,441 +0,0 @@
|
||||
# 화면설계서: 이전기본계획조회 (기본 계획 찾기)
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | --------------------------------------------------------------- |
|
||||
| 버전 | 0.2 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 이전기본계획조회 (기본 계획 찾기) |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 하수도기본계획 등록·수정 > 사업개요 > [이전기본계획조회] Dialog |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
하수도기본계획 등록·수정 화면에서 **이전(기수립) 기본계획** 수치와 관련 보고서를 **조회·다운로드**한다. 시도·구군·연도·처리시설을 선택한 뒤 총설·처리구역별 현황을 표시하고, 분류 트리에서 기본계획 파일을 내려받는다. **부모 사업개요 폼에는 수치를 반영하지 않는다**(레거시 `bscPlnPopup.jsp`와 동일).
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
부모: 하수도기본계획 등록/수정 폼 → 사업개요 섹션의 `[이전기본계획조회]` 버튼으로 Dialog 오픈.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Dialog 타이틀: 기본 계획 찾기 [닫기] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 시도 ▼ 구군 ▼ 연도 ▼ 처리시설 ▼ [조회] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 기본계획파일 │
|
||||
│ ┌──────────────────────┬────────────────────────────────────────────────────┐ │
|
||||
│ │ [영역 B-1] 분류 트리 │ [영역 B-2] 파일 목록 │ │
|
||||
│ │ (약 40%) │ (약 60%) │ │
|
||||
│ │ └ 노드… │ 파일명 / 다운로드 │ │
|
||||
│ └──────────────────────┴────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 C] 총설 │
|
||||
│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
|
||||
│ │ 구분 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ 기준 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ │
|
||||
│ ├────────┼───────┼───────┼───────┼───────┼────────┼───────┼───────┼───────┼───────┤ │
|
||||
│ │계획목표│ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ │
|
||||
│ │년도 │ │ │ │ │ │ │ │ │ │ │
|
||||
│ └────────┴───────┴───────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 D] 처리구역별 현황 │
|
||||
│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
|
||||
│ │ 구분 │ 1~4단계(+연도헤더) │ 기준 │ 1~4단계(+연도헤더) │ │
|
||||
│ ├────────┼────────────────────────────────┼────────┼────────────────────────────────┤ │
|
||||
│ │ 하수처리인구 … 배제방식 (읽기 전용 행) │ │
|
||||
│ └────────┴────────────────────────────────┴────────┴────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 부모 폼 — 사업개요
|
||||
ON [이전기본계획조회] 버튼 CLICK DO
|
||||
OPEN PreviousPlanLookupDialog
|
||||
// 프리필: 부모 폼 sewerBscPlan.sido / gugun / admdstCd
|
||||
CALL findSidoCombo()
|
||||
DISPLAY 시도 옵션 (영역 A)
|
||||
IF 시도 프리필 THEN
|
||||
CALL findGugunCombo(sidoCd)
|
||||
DISPLAY 구군 옵션 (영역 A)
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
|
||||
// TBD: useLvNowDmgMtncGnsvy enabled — level별 필수 파라미터만 검사하도록 조정
|
||||
IF 구군 프리필 THEN
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
|
||||
DISPLAY 연도 옵션 (영역 A) // children 중 level=3 노드의 gnsvyYr
|
||||
CALL getClsfSystm({ clsfSystmNm: gugunNm })
|
||||
DISPLAY 분류 트리 (영역 B-1)
|
||||
END IF
|
||||
END IF
|
||||
END ON
|
||||
|
||||
// 영역 A: 시도 변경
|
||||
ON 영역 A 시도 CHANGE DO
|
||||
RESET 구군 / 연도 / 처리시설
|
||||
CALL findGugunCombo(sidoCd)
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
|
||||
// 구군 미선택 → 연도·처리시설 옵션 비움, 트리 빈 안내
|
||||
END ON
|
||||
|
||||
// 영역 A: 구군 변경
|
||||
ON 영역 A 구군 CHANGE DO
|
||||
RESET 연도 / 처리시설
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
|
||||
DISPLAY 연도 옵션 (영역 A) // level=3 노드 → gnsvyYr
|
||||
CALL getClsfSystm({ clsfSystmNm: sggNm })
|
||||
DISPLAY 분류 트리 (영역 B-1)
|
||||
END ON
|
||||
|
||||
// 영역 A: 연도 변경
|
||||
ON 영역 A 연도 CHANGE DO
|
||||
RESET 처리시설
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })
|
||||
DISPLAY 처리시설 옵션 (영역 A) // level=3 노드 children → swtrmtFcltCd / swtrmtFcltNm
|
||||
END ON
|
||||
|
||||
// 영역 A: 조회
|
||||
ON [조회] 버튼 CLICK DO
|
||||
VALIDATE 시도, 구군, 연도(targetYear), 처리시설(swtrmtFcltCd)
|
||||
CALL getNowDmgMtncGnsvy({ swtrmtFcltCd, ctpvNm, sggNm, targetYear })
|
||||
DISPLAY 총설 (영역 C) // 응답[0]의 planYr / planYrDt* / planYrDc*
|
||||
DISPLAY 처리구역별 현황 (영역 D) // 응답[0]의 지표 Dt1~4 / base / Dc1~4
|
||||
END ON
|
||||
|
||||
// 영역 B: 트리 노드 선택
|
||||
ON 영역 B-1 노드 CLICK DO
|
||||
CALL getFile({ clsfSystmSn, sort, vetPrcsYn }) // Response DTO TBD
|
||||
DISPLAY 파일 목록 (영역 B-2)
|
||||
END ON
|
||||
|
||||
// 영역 B: 파일 다운로드
|
||||
ON 영역 B-2 파일 CLICK DO
|
||||
CALL getFilePrcs(fileSn) // 파일 메타(경로·제목) — Response TBD
|
||||
CALL fileDownload({ filePath, fileName })
|
||||
// 브라우저 다운로드 트리거
|
||||
END ON
|
||||
|
||||
ON [닫기] DO
|
||||
CLOSE PreviousPlanLookupDialog
|
||||
// 부모 폼 값 변경 없음
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **시도 · 구군 · 연도 · 처리시설** (+ 조회).
|
||||
|
||||
- 시도·구군: `adstdstr` 콤보 (기존 스펙 유지)
|
||||
- 연도·처리시설: 각각 `useLvNowDmgMtncGnsvy`를 **별도 호출**. `level`에 따라 반환 계층이 달라진다.
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | --------- | ---- | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| 시도 | Select | Y | 부모 `sewerBscPlan.sido` | `ctpvNm`(표시명), 코드는 `adstDstrCd` | `useFindSidoCombo` |
|
||||
| 구군 | Select | Y | 부모 `sewerBscPlan.gugun` | `sggNm`(표시명), 코드는 `adstDstrCd` | `useFindGugunCombo(sidoCd)` |
|
||||
| 연도 | Select | Y | 없음 | `gnsvyYr` / 조회 시 `targetYear` | `useLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })` |
|
||||
| 처리시설 | Select | Y | 없음 | `swtrmtFcltCd` ← 옵션 value, 라벨 `swtrmtFcltNm` | `useLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })` |
|
||||
|
||||
> 처리시설은 `bns-if` `useFacilityList`를 **사용하지 않는다**.
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------------------------- |
|
||||
| 조회 | 필수값 검증 후 `getNowDmgMtncGnsvy` 호출 → 영역 C·D 갱신 |
|
||||
| 닫기 | Dialog 종료 (부모 미반영) |
|
||||
|
||||
### 4-3. `lvNowDmgMtncGnsvy` level 캐스케이드
|
||||
|
||||
| 트리거 | Request `level` | Request 파라미터 | UI에 쓰는 응답 | 옵션 추출 |
|
||||
| --------- | --------------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------ |
|
||||
| 시도 선택 | `"1"` | `ctpvNm` | (하위 초기화용) | 연도·처리시설 Select 리셋. 구군은 `findGugunCombo` |
|
||||
| 구군 선택 | `"2"` | `ctpvNm`, `sggNm` | 연도 Select | 트리에서 `level === 3` 노드의 `gnsvyYr` |
|
||||
| 연도 선택 | `"3"` | `ctpvNm`, `sggNm`, `gnsvyYr` | 처리시설 Select | 선택 연도(`level === 3`) 노드의 `children[]` → `swtrmtFcltCd` / `swtrmtFcltNm` |
|
||||
|
||||
상위 변경 시 하위 선택값·옵션을 RESET한다 (시도→구군·연도·시설 / 구군→연도·시설 / 연도→시설).
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 기본계획파일
|
||||
|
||||
### 5-1. 트리 (B-1)
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------------- | ------------------------ |
|
||||
| 분류명 | `clsfSystmNm` | `ClsfSystmResponseDto` |
|
||||
| 노드키 | `clsfSystmSn` | 선택 시 파일 목록 조회 |
|
||||
| 상위키 | `upClsfSystmSn` | 클라이언트에서 트리 조립 |
|
||||
| 정렬 | `sortSeq` | |
|
||||
| 경로 | `path` | |
|
||||
|
||||
- 조회 파라미터: `clsfSystmNm` = 선택 구군명(레거시와 동일). `upClsfSystmSn`은 하위 전개 시 TBD.
|
||||
- 시도·구군 미선택 시: 「시도와 구군을 선택하세요.」 안내.
|
||||
|
||||
### 5-2. 파일 목록 (B-2)
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | ----- | ------------------------------------------------------------------- |
|
||||
| 파일명 | TBD | `getFile` Response DTO가 `unknown` — 백엔드 스키마 확정 후 매핑 |
|
||||
| (기타) | TBD | 레거시: 트리 선택 후 목록 표시, 클릭 시 `fileprcs` → `fileDownload` |
|
||||
|
||||
### 5-3. 버튼·상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| -------------- | -------------------------------------------------------------- |
|
||||
| 트리 노드 클릭 | `getFile({ clsfSystmSn, … })` → B-2 목록 |
|
||||
| 파일 클릭 | `getFilePrcs(fileSn)` → `fileDownload({ filePath, fileName })` |
|
||||
| 빈 목록 | 「트리에서 항목을 선택하세요.」 / 「파일이 없습니다.」 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 영역 C — 총설
|
||||
|
||||
### 6-1. 테이블 구조
|
||||
|
||||
| 행(구분) | 열 매핑 (당초1~~4 / 기준 / 변경1~~4) |
|
||||
| ------------ | ------------------------------------------------------------------------------ |
|
||||
| 계획목표년도 | `planYrDt1planYrDt4` ~~/~~ `planYr` ~~/~~ `planYrDc1planYrDc4` (+ `년` 접미사) |
|
||||
|
||||
- 헤더: 당초(1~~4단계) · 기준 · 변경(1~~4단계)
|
||||
- 데이터 소스: `getNowDmgMtncGnsvy` 응답 배열의 **첫 번째** 요소
|
||||
- 읽기 전용
|
||||
|
||||
---
|
||||
|
||||
## 7. 영역 D — 처리구역별 현황
|
||||
|
||||
### 7-1. 행 정의 (표시명 → Field 접두사)
|
||||
|
||||
각 행은 `Dt1Dt4` ~~/ (접미사 없음=기준) /~~ `Dc1Dc4` 셀로 표시한다. 숫자는 천단위 콤마, 없으면 `-`.
|
||||
|
||||
| 표시명 | Field 접두사 | 비고 |
|
||||
| ------------------------ | -------------------- | ---------------------------------------------- |
|
||||
| 하수처리인구(인) | `swtrmtPpltnCnt` | |
|
||||
| 자연적하수처리인구수(인) | `natrSwtrmtPpltnCnt` | |
|
||||
| 사회적하수처리인구수(인) | `scilSwtrmtPpltnCnt` | |
|
||||
| 하수처리구역면적 | `swtrmtZoneArea` | |
|
||||
| 물사용량원단위 | `wtrUsqtyOrgnUnit` | |
|
||||
| 오수전환율(%) | `drtwSwtcRt` | |
|
||||
| 변동부하율(%) | `flctnLodRt` | |
|
||||
| 오수원단위 | `drtwOrgnUnit` | |
|
||||
| 계획하수량합계(㎥/일) | `planSwageQntSum` | |
|
||||
| 생활오수(㎥/일) | `lvngDrtwSum` | |
|
||||
| 지하수(㎥/일) | `ugwtSum` | |
|
||||
| 공장폐수(㎥/일) | `fctryWwtSum` | |
|
||||
| 관광오수(㎥/일) | `toursmDrtwSum` | |
|
||||
| 군부대(㎥/일) | `mtbsSum` | |
|
||||
| 연계처리수(㎥/일) | `linkPrcsCntSum` | |
|
||||
| 기타오수(㎥/일) | `etcDrtwSum` | |
|
||||
| 지하수사용량(㎥/일) | `ugwtUseSum` | |
|
||||
| 시설용량(㎥/일) | `fcltCpctSum` | |
|
||||
| 배제방식 | `exclsMethExpln` | 문자열; 단계별 `*Dt*`/`*Dc*` 존재 시 동일 규칙 |
|
||||
|
||||
헤더 3행째 연도: 영역 C와 동일하게 `planYrDt*` / `planYr` / `planYrDc*` 표시.
|
||||
|
||||
---
|
||||
|
||||
## 8. API/DTO 명세
|
||||
|
||||
### 8-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| DC Base | `/dc-service/api/v1` (`ENDPOINT.DC_SERVICE`) |
|
||||
| AS adstdstr | `/as-service/api/v1/adstdstr` |
|
||||
| 클라이언트 모듈 | `@/entities/dc` (`dcService`), `@/entities/adstdstr` |
|
||||
|
||||
> 영역 A 처리시설은 `bns-if`를 사용하지 않음.
|
||||
|
||||
### 8-2. 시도·구군 콤보 (non-DC)
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------- | ------ | ----------------------------------- | ------------------------ | -------------------------------- | ---------------------------------------------------------- |
|
||||
| 시도목록 | GET | `/as-service/api/v1/adstdstr/sido` | — | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findSidoCombo` / `useFindSidoCombo` |
|
||||
| 구군목록 | GET | `/as-service/api/v1/adstdstr/gugun` | Query: `sido` (시도코드) | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findGugunCombo` / `useFindGugunCombo` |
|
||||
|
||||
### 8-3. 연도·처리시설 계층 조회 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| --------------- | ------ | -------------------------------------- | ---------------------------- | -------------------------------- | --------------------------------------------------------- |
|
||||
| 계층형 계획목록 | GET | `/dc-service/api/v1/lvNowDmgMtncGnsvy` | `LvNowDmgMtncGnsvySearchDto` | `LvNowDmgMtncGnsvyResponseDto[]` | `dcService.getLvNowDmgMtncGnsvy` / `useLvNowDmgMtncGnsvy` |
|
||||
|
||||
연도 Select·처리시설 Select는 **각각** `useLvNowDmgMtncGnsvy`를 호출한다 (`level`만 다르게).
|
||||
|
||||
**Request (**`LvNowDmgMtncGnsvySearchDto`**)**
|
||||
|
||||
| 필드 | 시도(`level=1`) | 구군→연도(`level=2`) | 연도→시설(`level=3`) |
|
||||
| --------- | --------------- | -------------------- | -------------------- |
|
||||
| `level` | `"1"` | `"2"` | `"3"` |
|
||||
| `ctpvNm` | 필수 | 필수 | 필수 |
|
||||
| `sggNm` | — | 필수 | 필수 |
|
||||
| `gnsvyYr` | — | — | 필수 |
|
||||
|
||||
**Response 노드 필드 (**`LvNowDmgMtncGnsvyResponseDto`**, 부분 optional)**
|
||||
|
||||
| 필드 | 등장 level | 설명 |
|
||||
| -------------- | ---------- | ------------------------ |
|
||||
| `level` | 1~3 | 계층 깊이 |
|
||||
| `ctpvNm` | 1 | 시도명 |
|
||||
| `sggNm` | 2 | 구군명 |
|
||||
| `gnsvyYr` | 3 | 연도 → 연도 Select value |
|
||||
| `swtrmtFcltCd` | 시설(리프) | 처리시설 코드 |
|
||||
| `swtrmtFcltNm` | 시설(리프) | 처리시설명 |
|
||||
| `children` | 1~3 | 하위 노드 배열 |
|
||||
|
||||
> **TBD**: 현재 `useLvNowDmgMtncGnsvy` `enabled`가 `level && ctpvNm && sggNm && gnsvyYr`로 고정되어 `level=1`/`2` 호출이 막힘. level별 필수 파라미터만 검사하도록 수정 필요.
|
||||
>
|
||||
> 예: `level=1` → `ctpvNm` / `level=2` → `ctpvNm && sggNm` / `level=3` → `ctpvNm && sggNm && gnsvyYr`
|
||||
|
||||
### 8-4. 기본계획 수치 조회 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ---------------- | ------ | ------------------------------------ | -------------------------- | ------------------------------ | ----------------------------------------------------- |
|
||||
| 현재·단계별 수치 | GET | `/dc-service/api/v1/nowDmgMtncGnsvy` | `NowDmgMtncGnsvySearchDto` | `NowDmgMtncGnsvyResponseDto[]` | `dcService.getNowDmgMtncGnsvy` / `useNowDmgMtncGnsvy` |
|
||||
|
||||
**Request (**`NowDmgMtncGnsvySearchDto`**)**
|
||||
|
||||
| 필드 | 매핑 |
|
||||
| -------------- | ----------------------- |
|
||||
| `swtrmtFcltCd` | 영역 A 처리시설 |
|
||||
| `ctpvNm` | 영역 A 시도 표시명 |
|
||||
| `sggNm` | 영역 A 구군 표시명 |
|
||||
| `targetYear` | 영역 A 연도 (`gnsvyYr`) |
|
||||
|
||||
**Response**: 지표 필드 + `Dt1Dt4` ~~/~~ `Dc1Dc4` 접미사 스테이지 필드 (`NowDmgMtncGnsvyResponseDto`). UI는 `data[0]` 사용.
|
||||
|
||||
### 8-5. 분류체계 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ------------ | ------ | ------------------------------ | -------------------- | ------------------------ | ----------------------------------------- |
|
||||
| 분류트리조회 | GET | `/dc-service/api/v1/clsfSystm` | `ClsfSystmParamsDto` | `ClsfSystmResponseDto[]` | `dcService.getClsfSystm` / `useClsfSystm` |
|
||||
|
||||
**Request**: `clsfSystmNm?`, `upClsfSystmSn?`
|
||||
**Response**: `clsfSystmSn`, `upClsfSystmSn`, `clsfSystmNm`, `sortSeq`, `droppAble`, `path`
|
||||
|
||||
> 레거시 경로 `/clsf` → 현재 클라이언트는 `/clsfSystm`.
|
||||
|
||||
### 8-6. 파일 목록·처리·다운로드 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO / Params | Response DTO | API |
|
||||
| ------------ | ------ | --------------------------------- | ------------------------------------------------------- | ----------------- | -------------------------------------------- |
|
||||
| 파일목록 | GET | `/dc-service/api/v1/file` | `FileParamsDto` (`sort?`, `clsfSystmSn?`, `vetPrcsYn?`) | `unknown` **TBD** | `dcService.getFile` / `useFile` |
|
||||
| 파일처리정보 | GET | `/dc-service/api/v1/fileprcs` | Query: `fileSn` | `unknown` **TBD** | `dcService.getFilePrcs` / `useFilePrcs` |
|
||||
| 파일다운로드 | POST | `/dc-service/api/v1/fileDownload` | `FileDownloadParamsDto` (`filePath?`, `fileName?`) | `unknown` **TBD** | `dcService.fileDownload` / `useFileDownload` |
|
||||
|
||||
> **TBD**: 파일 관련 Response 스키마·다운로드 blob/헤더 처리·`fileDownload` POST body vs query 전달 방식은 백엔드/게이트웨이 확인 후 확정.
|
||||
|
||||
---
|
||||
|
||||
## 9. UI 구현 가이드
|
||||
|
||||
### 9-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — `Dialog`, `Select`, `Button`, `Table`, `Skeleton`
|
||||
- 상태: Dialog 로컬 state + React Query (`entities/dc`, `adstdstr`)
|
||||
- 영역 A 연도·처리시설: `useLvNowDmgMtncGnsvy`를 level별 **두 번**(또는 시도 포함 시 세 번) 호출
|
||||
- 테이블: 읽기 전용 고정 행 → `@tanstack/react-table` / `DataTable` **불필요**
|
||||
- 트리: jsTree 미사용 — 재귀 들여쓰기 트리
|
||||
- 번들: `PreviousPlanLookupDialog`는 outline에서 `next/dynamic` 로드 권장
|
||||
- 부모 폼: **수치 미반영** (조회·다운로드 only)
|
||||
- 1차 구현 시 DC 미연동이면 `@faker-js/faker` mock + TODO → 이후 `dcService` 훅으로 교체
|
||||
|
||||
### 9-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| 진입 버튼 | `pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx` |
|
||||
| Dialog | `pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx` |
|
||||
| 필터 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx` |
|
||||
| 파일 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx` |
|
||||
| 총설 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-summary-table.tsx` |
|
||||
| 처리구역 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-area-table.tsx` |
|
||||
| 셀 매핑 유틸 | `pages/sewer-bsc-plan/lib/previous-plan-table-utils.ts` |
|
||||
| mock(선택) | `pages/sewer-bsc-plan/lib/previous-plan-mock.ts` |
|
||||
| DC entity | `entities/dc/api/dc-service.ts`, `queries.ts`, `model/dc-interface.ts` |
|
||||
| 레거시 참조 | `adssNewOld/.../cmmn/bscPlnPopup.jsp` |
|
||||
|
||||
### 9-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------------------ | -------------------------------------------------------------- |
|
||||
| A | `previous-plan-search-filters` | adstdstr + `useLvNowDmgMtncGnsvy`(level 1·2·3) |
|
||||
| B | `previous-plan-file-section` | `useClsfSystm` + `useFile` + `useFilePrcs` + `useFileDownload` |
|
||||
| C | `previous-plan-summary-table` | `useNowDmgMtncGnsvy` / 조회 결과 state |
|
||||
| D | `previous-plan-area-table` | 동일 |
|
||||
|
||||
### 9-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| 다이얼로그명 | `PreviousPlanLookupDialog` / 타이틀 `기본 계획 찾기` |
|
||||
| 오픈 | 사업개요 `[이전기본계획조회]` |
|
||||
| 라우트 | 없음 (모달). 부모: `/sewer-bsc-plan/regist`, `/sewer-bsc-plan/[id]/edit` |
|
||||
| 선택 반영 | 없음 |
|
||||
|
||||
### 9-5. 데이터 흐름 (구현 요약)
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
ON [이전기본계획조회] CLICK DO
|
||||
OPEN PreviousPlanLookupDialog (sido/gugun 프리필)
|
||||
END ON
|
||||
|
||||
ON 시도 CHANGE DO
|
||||
CALL findGugunCombo
|
||||
CALL getLvNowDmgMtncGnsvy(level=1)
|
||||
RESET 연도·처리시설
|
||||
END ON
|
||||
|
||||
ON 구군 CHANGE DO
|
||||
CALL getLvNowDmgMtncGnsvy(level=2) // → 연도 옵션
|
||||
CALL getClsfSystm
|
||||
RESET 처리시설
|
||||
END ON
|
||||
|
||||
ON 연도 CHANGE DO
|
||||
CALL getLvNowDmgMtncGnsvy(level=3) // → 처리시설 옵션
|
||||
END ON
|
||||
|
||||
ON [조회] CLICK DO
|
||||
VALIDATE
|
||||
CALL getNowDmgMtncGnsvy
|
||||
DISPLAY 영역 C · 영역 D
|
||||
END ON
|
||||
|
||||
ON 트리 노드 / 파일 CLICK DO
|
||||
CALL getFile → getFilePrcs → fileDownload // Response TBD
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 미확정 (TBD) 추적
|
||||
|
||||
| ID | 항목 | 비고 |
|
||||
| --- | -------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| T1 | `useLvNowDmgMtncGnsvy` enabled을 level별 필수값으로 분기 | `1`: ctpvNm / `2`: +sggNm / `3`: +gnsvyYr |
|
||||
| T2 | `getFile` / `getFilePrcs` / `fileDownload` Response DTO | 현재 `unknown` |
|
||||
| T3 | 파일 다운로드 UX (blob, Content-Disposition, 게이트웨이) | 레거시 apigateway 직접 호출과 차이 |
|
||||
| T4 | `clsfSystm`의 `upClsfSystmSn` 점진 로드 여부 | flat 목록 vs 계층 API |
|
||||
| T5 | 1차 mock vs 즉시 `dcService` 실연동 | 마이그레이션 플랜은 mock 우선, entity는 준비됨 |
|
||||
| T6 | 부모 폼 수치 반영 | 요구 없음(레거시 동일). 요청 시 별도 스펙 |
|
||||
| T7 | `level=1` 응답 UI 활용 여부 | 시도 선택 시 호출하나 구군은 adstdstr — 응답은 캐시·검증용일 수 있음 |
|
||||
@@ -1,370 +0,0 @@
|
||||
# 화면설계서: 검토인력관리
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 0.2 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 검토인력관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [ ] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 검토인력관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
조직도에서 **한 명의 사용자**를 선택한 뒤, 결재 단계(접수·검토·승인·최종승인)별 **인원 배치** 영역에 즉시 등록한다. 등록·목록 갱신은 별도 저장 버튼 없이 **[선택 인원 배치]** 클릭 시 `rvw-mnpw` API로 바로 반영한다. 좌측 조직도는 CMS 조직·사용자 트리 API를, 우측 배치 목록은 검토인력관리 API를 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 페이지 타이틀: 검토인력관리 브레드크럼: 디지털행정지원 > 사이트관리 > 검토인력관리 │
|
||||
├─────────────────────────────────┬──────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 조직도 │ [영역 B] 인원 배치 (결재구분별 4블록, 세로 스택) │
|
||||
│ 제목: 조직도 │ │
|
||||
│ [A-1] 검색: │ ┌─ [B-1] 접수 ───────────── count ─ [선택 인원 배치] ─┐ │
|
||||
│ [성명·부서 검색] [조회][초기화]│ │ (배치 목록 또는 빈 상태 문구) │ │
|
||||
│ 선택 안내: │ └──────────────────────────────────────────────────────┘ │
|
||||
│ "선택된 인원: {이름} │ ┌─ [B-2] 검토 ───────────── count ─ [선택 인원 배치] ──┐ │
|
||||
│ ({부서} / {직급})" │ └──────────────────────────────────────────────────────┘ │
|
||||
│ [A-2] 조직 트리 + 사용자 행 │ ┌─ [B-3] 승인 ───────────── count ─ [선택 인원 배치] ──┐ │
|
||||
│ ┌────────────────────┐ │ └──────────────────────────────────────────────────────┘ │
|
||||
│ │ 조직 트리 + 사용자 │ │ ┌─ [B-4] 최종승인 ───────── count ─ [선택 인원 배치] ──┐ │
|
||||
│ │ (단일 선택 강조) │ │ └──────────────────────────────────────────────────────┘ │
|
||||
│ └────────────────────┘ │ │
|
||||
└─────────────────────────────────┴──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 최초 진입
|
||||
CALL userOgnzService.findOgnzUserTree()
|
||||
DISPLAY 조직도 트리 (영역 A)
|
||||
|
||||
CALL rvwMnpwMngService.findPage({ size: 충분히 큰 값 }) // 또는 atrzSeComCd별 분할 조회
|
||||
GROUP BY atrzSeComCd
|
||||
DISPLAY 인원 배치 4블록 (영역 B-1 ~ B-4) + 건수 뱃지
|
||||
|
||||
// 영역 A-1: 조직도 검색 (클라이언트 필터 — 서버 API 없음)
|
||||
ON [조회] 버튼 CLICK / 검색 폼 SUBMIT DO
|
||||
SET displayTree = filterOgnzUserTree(원본트리, keyword)
|
||||
SET expandedOgnzSnIds = collectExpandedOgnzSnIds(displayTree) // 매칭 노드 자동 펼침
|
||||
DISPLAY 필터된 트리 (영역 A-2)
|
||||
END ON
|
||||
|
||||
ON [초기화] 버튼 CLICK DO
|
||||
RESET keyword = ""
|
||||
RESET displayTree = 원본트리
|
||||
RESET expandedOgnzSnIds = 빈 Set (또는 기본 접힘)
|
||||
DISPLAY 전체 트리 (영역 A-2)
|
||||
END ON
|
||||
|
||||
// 영역 A-2: 사용자 단일 선택
|
||||
ON 조직도 사용자 행 CLICK DO
|
||||
VALIDATE 동일 행 재클릭이 아닌 경우만 변경
|
||||
SET selectedPerson = { userUuid, userNm, ognzSn, ognzNm, jbgdSeCd, ... }
|
||||
DISPLAY "선택된 인원: {userNm} ({ognzNm} / {직급명})"
|
||||
END ON
|
||||
|
||||
// 영역 B-k: 선택 인원 즉시 배치
|
||||
ON [B-k] [선택 인원 배치] 버튼 CLICK DO
|
||||
VALIDATE selectedPerson 존재
|
||||
SET targetAtrzSeComCd = 블록 k 에 매핑된 결재구분 코드 (§4-2 표)
|
||||
CALL rvwMnpwMngService.save({
|
||||
atrzSeComCd: targetAtrzSeComCd,
|
||||
ognzSn: selectedPerson.ognzSn,
|
||||
userUuid: selectedPerson.userUuid
|
||||
})
|
||||
ON 성공 DO
|
||||
CALL rvwMnpwMngService.findPage (또는 해당 블록 목록만 갱신)
|
||||
DISPLAY [B-k] 배치 목록·건수 뱃지 갱신
|
||||
TOAST "배치되었습니다."
|
||||
END ON
|
||||
ON 실패(이미 존재) DO
|
||||
TOAST (백엔드 메시지 연동)
|
||||
END ON
|
||||
END ON
|
||||
|
||||
// 영역 B-k: 배치 인원 제거 (UI 상 삭제 UX — 스크린샷 미표기, API는 제공됨)
|
||||
ON 배치 인원 [삭제] / [제거] CLICK DO
|
||||
CONFIRM "해당 인원을 배치에서 제거하시겠습니까?"
|
||||
CALL rvwMnpwMngService.delete(atrzSeComCd, ognzSn, userUuid)
|
||||
CALL rvwMnpwMngService.findPage
|
||||
DISPLAY [B-k] 목록·뱃지 갱신
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 조직도
|
||||
|
||||
### 4-1. 표시·상호작용
|
||||
|
||||
| 요소 | 내용 |
|
||||
| ---------------- | --------------------------------------------------------------------- |
|
||||
| 섹션 제목 | 조직도 |
|
||||
| 검색 폼 (A-1) | 성명·부서 키워드 입력 + **[조회]** + **[초기화]** — §4-3 |
|
||||
| 선택 안내 문구 | `선택된 인원: {userNm} ({ognzNm} / {직급표시})` — 미선택 시 안내 문구 |
|
||||
| 트리 (A-2) | 본부 → 팀 계층 + 팀 소속 사용자 목록 (`displayTree` 기준 표시) |
|
||||
| 선택 규칙 | **사용자 1명만** 선택 가능 (라디오형 단일 선택) |
|
||||
| 선택 시각 피드백 | 선택된 사용자 행 강조(배경·테두리) |
|
||||
| 로딩/오류/빈값 | Skeleton / 오류 문구 / "등록된 조직/사용자가 없습니다." |
|
||||
| 검색 결과 없음 | 필터 후 매칭 노드·사용자 0건. "조회된 인원이 없습니다." 문구 노출 |
|
||||
|
||||
### 4-2. 조직도 데이터 매핑
|
||||
|
||||
| UI 표시 | 트리 DTO 필드 (`OgnzUserTreeResponseDto`) | 비고 |
|
||||
| --------------- | ----------------------------------------- | ----------------------------------------- |
|
||||
| 조직명 | `ognzNm` | 접기/펼치기 노드 |
|
||||
| 사용자명 | `users[].userNm` | 클릭 시 선택 |
|
||||
| 부서(안내 문구) | `ognzNm` (소속 팀 노드) | 선택 시 `selectedPerson.ognzNm` |
|
||||
| 직급(안내 문구) | `users[].jbgdSeCd` 또는 `jbpsSeCd` | 공통코드 라벨 변환 |
|
||||
| 저장용 조직 SN | `ognzSn` | 사용자가 속한 **리프/팀 노드**의 `ognzSn` |
|
||||
| 저장용 사용자 | `users[].userUnqId` | API body `userUuid`로 전달 (§6-3) |
|
||||
|
||||
### 4-3. 조직도 검색 (영역 A-1)
|
||||
|
||||
결재선 관리 `AprvrPickerDialog`와 동일한 **클라이언트 사이드 필터** 패턴을 사용한다. 별도 검색 API는 없으며, `useOgnzUserTree`로 받은 **원본 트리**를 `filterOgnzUserTree`로 가공해 `displayTree`에 반영한다.
|
||||
|
||||
#### 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------------- | --------- | ---- | ------ | ----------------- | ----------------------------------------- |
|
||||
| 성명·부서 검색 | Text | N | (빈값) | — (클라이언트만) | `placeholder="성명·부서 검색"` |
|
||||
| 조회 | Button | — | — | — | `type="submit"` — 폼 제출 시 필터 적용 |
|
||||
| 초기화 | Button | — | — | — | `type="button"` — 키워드·트리·펼침 초기화 |
|
||||
|
||||
#### 필터 규칙 (`filterOgnzUserTree`)
|
||||
|
||||
| 대상 | 매칭 조건 |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| 조직명 | `ognzNm`에 키워드 포함 (대소문자 무시) |
|
||||
| 사용자명 | `userNm`에 키워드 포함 (대소문자 무시) |
|
||||
| 조직 매칭 시 | 해당 노드 **소속 사용자 전원** 유지 |
|
||||
| 사용자만 매칭 시 | 매칭 사용자만 남기고 상위 조직 노드는 유지(경로 보존) |
|
||||
| 하위 조직 | 재귀 필터 — 자식·손자 노드 매칭 시 상위도 표시 |
|
||||
| 키워드 빈값 | 원본 트리 전체 반환 |
|
||||
|
||||
#### 조회 후 트리 펼침 (`collectExpandedOgnzSnIds`)
|
||||
|
||||
- **[조회]** 성공 후 필터 결과 트리의 **모든 노드** `ognzSn`을 `expandedOgnzSnIds`에 넣어 자동 펼침
|
||||
- 검색 결과에서 사용자·하위 조직을 바로 확인할 수 있도록 함
|
||||
|
||||
#### 초기화 동작
|
||||
|
||||
| 상태 | 초기화 후 값 |
|
||||
| ------------------- | --------------------------------------- |
|
||||
| `keyword` | `""` |
|
||||
| `displayTree` | `useOgnzUserTree` 원본 `data` |
|
||||
| `expandedOgnzSnIds` | `new Set()` (전체 접힘) |
|
||||
| `selectedPerson` | **유지** — 검색 초기화와 선택 해제 분리 |
|
||||
|
||||
#### 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | --------------------------------------------------------- |
|
||||
| 조회 | `filterOgnzUserTree(data, keyword)` → `displayTree` 갱신 |
|
||||
| 초기화 | 키워드·표시 트리·펼침 상태 리셋 (§3-2 `ON [초기화]` 블록) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 인원 배치
|
||||
|
||||
### 5-1. 블록 공통
|
||||
|
||||
| 요소 | 내용 |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| 블록 제목 | 접수 / 검토 / 승인 / 최종승인 |
|
||||
| 건수 뱃지 | 해당 `atrzSeComCd`에 배치된 인원 수 |
|
||||
| 액션 버튼 | **[선택 인원 배치]** — 클릭 시 즉시 `save` (§3-2) |
|
||||
| 빈 상태 | "배치된 인원이 없습니다." |
|
||||
| 배치 목록(있을 때) | 인원명·부서·직급 등 |
|
||||
|
||||
### 5-2. 결재구분 ↔ 블록 매핑
|
||||
|
||||
| 블록 | UI 제목 | `atrzSeComCd` | 비고 |
|
||||
| ---- | -------- | ------------- | -------------------------------------------------------------- |
|
||||
| B-1 | 접수 | `ATRZ00` | 결재선 관리 `ATRZ_RECEIVER_CD`와 동일 |
|
||||
| B-2 | 검토 | `ATRZ01` | 결재선 `ATRZ_REVIEWER_1_CD`. `ATRZ02` 별도 블록 여부 — **TBD** |
|
||||
| B-3 | 승인 | `ATRZ03` | 결재선 `ATRZ_APPROVER_CD` |
|
||||
| B-4 | 최종승인 | `ATRZ99` | 결재선 `ATRZ_FINAL_CD` |
|
||||
|
||||
공통코드 마스터 `ATRZSE` / 화면 셀렉트용 `ATRZSECD`와 정합성은 배포 환경 코드표로 최종 확인.
|
||||
|
||||
### 5-3. 배치 목록 항목 (표시)
|
||||
|
||||
| 표시명 | Field (응답 DTO) | 비고 |
|
||||
| -------- | ---------------- | ---------------------------- |
|
||||
| 사용자명 | `userUuidNm` | `@CodeName` 미적용 시 TBD |
|
||||
| 결재구분 | `atrzSeComCdNm` | 블록 제목과 중복 — 생략 가능 |
|
||||
| 조직 SN | `ognzSn` | 삭제 API path에 사용 |
|
||||
| 사용자 | `userUuid` | 삭제 API path에 사용 |
|
||||
|
||||
### 5-4. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| -------------- | ----------- | --------------------------------- |
|
||||
| 선택 인원 배치 | 각 B-k 헤더 | `save` 후 목록·뱃지 갱신 (§3-2) |
|
||||
| 제거 | 목록 항목 | `delete` 후 목록·뱃지 갱신 (§3-2) |
|
||||
|
||||
### 5-5. 검증·제약
|
||||
|
||||
| 규칙 | 내용 |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| 선택 필수 | `[선택 인원 배치]` 시 `selectedPerson` 없으면 버튼 비활성 또는 안내 |
|
||||
| 단일 선택 | 조직도에서 사용자 1명만 `selectedPerson` 유지 |
|
||||
| 중복 등록 | 동일 PK `(atrzSeComCd, ognzSn, userUuid)` 재등록 시 서버 오류 |
|
||||
| 다건 배치 | 동일 블록에 서로 다른 사용자·조직은 **복수 행** 가능 (PK가 다름) |
|
||||
| 결재구분 변경 | 본 화면 주 흐름은 `save`만 |
|
||||
|
||||
---
|
||||
|
||||
## 6. API/DTO 명세
|
||||
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| 인증 | JWT (`Authorization` 헤더) |
|
||||
| 페이징 | Spring `Pageable`: `page`, `size`, `sort` → `PageResponse<T>` |
|
||||
| 프론트 엔티티 | `@/entities/user-organization`, `@/entities/rvw-mnpw` |
|
||||
|
||||
### 6-2. 조직·사용자 트리 (영역 A) — `user-ognz`
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------- |
|
||||
| 서비스 | `cms-service` |
|
||||
| Base prefix | `/cms-service/api/v1/user-ognz` |
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ---------------- | ------ | ----------- | ----------- | --------------------------- | ---------------------------------- |
|
||||
| 조직·사용자 트리 | GET | `/treeuser` | — | `OgnzUserTreeResponseDto[]` | `userOgnzService.findOgnzUserTree` |
|
||||
|
||||
**`OgnzUserTreeResponseDto`**: `ognzSn`, `upOgnzSn`, `ognzNm`, `users[]`, `children[]`
|
||||
|
||||
**`OgnzUserTreeUserDto`**: `userUnqId`, `userNm`, `jbpsSeCd`, `jbgdSeCd`
|
||||
|
||||
**React Query**: `useOgnzUserTree`
|
||||
|
||||
### 6-3. 검토인력관리 (영역 B) — `rvw-mnpw`
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------- |
|
||||
| 서비스 | `as-service` |
|
||||
| Base prefix | `/as-service/api/v1/rvw-mnpw` |
|
||||
| ENDPOINT | `ENDPOINT.AS_COMMON_SERVICE.RVW_MNPW` |
|
||||
|
||||
| 기능 | Method | URL | Request DTO / Query | Response DTO | API |
|
||||
| --------------- | ------ | ------------------------------------ | -------------------------------- | ----------------------------- | ---------------------------- |
|
||||
| 목록 조회 | GET | `/` | `RvwMnpwMngSearchDto` + Pageable | `Page<RvwMnpwMngResponseDto>` | `rvwMnpwMngService.findPage` |
|
||||
| 상세 조회 | GET | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.findById` |
|
||||
| 등록(즉시 배치) | POST | `/` | `RvwMnpwMngSaveRequestDto` | `RvwMnpwMngResponseDto` (201) | `rvwMnpwMngService.save` |
|
||||
| 결재구분 변경 | PUT | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | `RvwMnpwMngUpdateRequestDto` | `RvwMnpwMngResponseDto` | `rvwMnpwMngService.update` |
|
||||
| 삭제 | DELETE | `/{atrzSeComCd}/{ognzSn}/{userUuid}` | Path | `string` | `rvwMnpwMngService.delete` |
|
||||
|
||||
**`RvwMnpwMngSearchDto`**: `atrzSeComCd?`, `ognzSn?`, `userUuid?` + `page`, `size`, `sort`
|
||||
|
||||
**`RvwMnpwMngSaveRequestDto`**: `atrzSeComCd*`, `ognzSn*`, `userUuid*`
|
||||
|
||||
**`RvwMnpwMngUpdateRequestDto`**: `newAtrzSeComCd*` — 본 화면 기본 흐름에서는 미사용
|
||||
|
||||
**`RvwMnpwMngResponseDto`**: `atrzSeComCd`, `atrzSeComCdNm?`, `ognzSn`, `userUuid`, `userUuidNm?`, `rgtr?`, `regDt?`
|
||||
|
||||
### 6-4. 조직도 ↔ 검토인력 필드 매핑 (저장 시)
|
||||
|
||||
| 조직도 (`OgnzUserTreeUserDto` + 노드) | `RvwMnpwMngSaveRequestDto` | 비고 |
|
||||
| ------------------------------------- | -------------------------- | --------------------- |
|
||||
| (블록 k의 `atrzSeComCd`) | `atrzSeComCd` | §5-2 표 |
|
||||
| 소속 팀 `ognzSn` | `ognzSn` | |
|
||||
| `userUnqId` | `userUuid` | 문자열(UUID)로 직렬화 |
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 구현 가이드
|
||||
|
||||
### 7-1. 기술 스택·원칙
|
||||
|
||||
- **공통 컴포넌트**: `@common/components` (`Button`, `Badge`, `Card` 또는 섹션 레이아웃)
|
||||
- **데이터**: React Query — `useOgnzUserTree`, `useRvwMnpwMngPage`, `useSaveRvwMnpwMng`, `useDeleteRvwMnpwMng`
|
||||
- **상태**: `selectedPerson`(단일), `keyword`, `displayTree`, `expandedOgnzSnIds`, 블록별 배치 목록(서버 데이터 파생)
|
||||
- **조직도 검색**: `filterOgnzUserTree`, `collectExpandedOgnzSnIds` (`widgets/ognz-user-tree/lib` 또는 페이지 `lib`)
|
||||
- **저장 UX**: 별도 화면 저장 없음 — **[선택 인원 배치]** = `save` mutation 즉시 호출
|
||||
- **조직도 UI**: 결재선 관리 `ognz-user-tree.tsx`를 **widgets**로 승격해 공통화
|
||||
|
||||
### 7-2. 모듈·파일 배치(제안)
|
||||
|
||||
| 구분 | 경로 |
|
||||
| -------- | ---------------------------------------------------------------------------------- |
|
||||
| 라우트 | `apps/as/app/(with-base-layout)/rvw-mnpw-mng/page.tsx` |
|
||||
| 페이지 | `apps/as/src/pages/rvw-mnpw-mng/` |
|
||||
| UI | `.../ui/rvw-mnpw-mng-page.tsx`, `rvw-mnpw-assignment-panel.tsx` 등 |
|
||||
| model | `.../model/selected-ognz-user.ts`, `rvw-mnpw-block-config.ts` (`atrzSeComCd` 상수) |
|
||||
| lib | `.../lib/group-rvw-mnpw-by-atrz-se.ts` (목록 → B-1~B-4 분배) |
|
||||
| **위젯** | `apps/as/src/widgets/ognz-user-tree/` — 아래 이전 대상 |
|
||||
| 엔티티 | `entities/user-organization/`, `entities/rvw-mnpw/` (구현 완료) |
|
||||
|
||||
#### widgets/ognz-user-tree 이전 (결재선 → 공통)
|
||||
|
||||
| 현재 (결재선 관리) | 이전 후 (widgets) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
||||
| `pages/approval-line-management/ui/ognz-user-tree.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree.tsx` |
|
||||
| `pages/.../ui/ognz-user-tree-node.tsx` | `widgets/ognz-user-tree/ui/ognz-user-tree-node.tsx` |
|
||||
| `pages/.../model/reviewer-option.ts` | `widgets/ognz-user-tree/model/selected-ognz-user.ts` (이름·필드 확장: `ognzSn` 필수) |
|
||||
| `pages/.../lib/filter-ognz-user-tree.ts` | `widgets/ognz-user-tree/lib/filter-ognz-user-tree.ts` (**필수** — 검색 공통화) |
|
||||
| `widgets/ognz-user-tree/index.ts` | `OgnzUserTree`, `OgnzUserTreeNode`, 타입 export |
|
||||
|
||||
결재선 `AprvrPickerDialog`는 `@/widgets/ognz-user-tree` import로 교체. 검토인력관리 페이지는 **인라인 조직도**(다이얼로그 없음)로 동일 위젯 사용.
|
||||
|
||||
**`SelectedOgnzUser`** — `ReviewerOption` 확장:
|
||||
|
||||
```typescript
|
||||
export interface SelectedOgnzUser {
|
||||
userUuid: string; // OgnzUserTreeUserDto.userUnqId
|
||||
userNm: string;
|
||||
ognzSn: number;
|
||||
ognzNm: string;
|
||||
jbgdSeCd?: string;
|
||||
jbpsSeCd?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 주요 UI |
|
||||
| ---- | -------------------------------------------------------------------- |
|
||||
| A-1 | 검색 폼 (`Input` + 조회/초기화) — `AprvrPickerDialog` 90~103행 패턴 |
|
||||
| A-2 | `OgnzUserTree(nodes={displayTree})` + 선택 안내 + `useOgnzUserTree` |
|
||||
| B-k | 제목·Badge(count)·`선택 인원 배치`·배치 목록(또는 empty) |
|
||||
| 공통 | 페이지 헤더·브레드크럼·2열 레이아웃 (`grid` / `flex`, 모바일은 스택) |
|
||||
|
||||
### 7-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | ----------------------------------- |
|
||||
| 다이얼로그 | **없음** (조직도 인라인, 즉시 저장) |
|
||||
| 라우팅 | 메뉴 `검토인력관리` 단일 페이지 |
|
||||
|
||||
### 7-5. 데이터 흐름(구현 요약)
|
||||
|
||||
- **진입**: `useOgnzUserTree` → `displayTree = data` + `useRvwMnpwMngPage({ page: 0, size: N })` → 4패널.
|
||||
- **검색**: `keyword` 로컬 상태 → **[조회]** 시 `filterOgnzUserTree` + `collectExpandedOgnzSnIds` → `OgnzUserTree`에 `displayTree` 전달.
|
||||
- **초기화**: `keyword`·`displayTree`·`expandedOgnzSnIds` 리셋 (`selectedPerson` 유지).
|
||||
- **선택**: 트리 `onSelect` → `selectedPerson` (반드시 `ognzSn` 포함).
|
||||
- **배치**: `useSaveRvwMnpwMng().mutate({ atrzSeComCd, ognzSn, userUuid })` → `onSuccess`에서 `rvw-mnpw` query invalidate.
|
||||
- **삭제**: 목록 항목에서 `useDeleteRvwMnpwMng` — UI 확정 후 연결.
|
||||
|
||||
### 7-6. 미확정 / 검증 필요
|
||||
|
||||
- [ ] `ATRZ02`(검토2) 별도 블록 필요 여부
|
||||
- [ ] 권한(역할별 CRUD)
|
||||
@@ -1,33 +0,0 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import baseEsLintConfig from "../../eslint.config.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...baseEsLintConfig,
|
||||
{
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-wrapper-object-types": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,88 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
PORT,
|
||||
PROXY_HOST,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
|
||||
const serverApiUrl = process.env.SERVER_API_URL;
|
||||
const dcServiceApiUrl = process.env.DC_SERVICE_API_URL;
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;");
|
||||
return response;
|
||||
}
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
const logMessages: string[] = [
|
||||
`[Middleware Request] ${new Date().toISOString()}`,
|
||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
|
||||
`[Request]\n Original Pathname: ${pathname}`,
|
||||
];
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
|
||||
let serverUrl: string | undefined;
|
||||
if (pathname.includes("/dc-service")) {
|
||||
serverUrl = dcServiceApiUrl;
|
||||
} else {
|
||||
serverUrl = serverApiUrl;
|
||||
}
|
||||
|
||||
const newUrl = `${serverUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
const response = NextResponse.rewrite(new URL(newUrl));
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
// tus-js-client PATCH 요청: Location 헤더로 /server 없이 반환된 경로 직접 처리
|
||||
// (예: /attach-service/api/v1/tus/upload/{id})
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` [TUS Proxy] Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
const response = NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
if (pathname === "/") {
|
||||
const response = NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
return response;
|
||||
}
|
||||
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
const response = NextResponse.next();
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/"],
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@common/assets"],
|
||||
experimental: {
|
||||
globalNotFound: true,
|
||||
},
|
||||
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "as",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3003",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3003",
|
||||
"lint": "next lint",
|
||||
"lint:fsd": "steiger ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/auth": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"axios": "^1.8.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.5.18",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.11",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# 이 폴더는 비워 둡니다 (FSD + Next.js App Router)
|
||||
|
||||
Next.js가 `src/pages`를 **Pages Router**용 `pages` 디렉터리로 인식하지 않도록,
|
||||
프로젝트 루트에 **빈 `pages` 폴더**를 두는 것이 FSD 공식 가이드 권장사항입니다.
|
||||
|
||||
- 실제 페이지(FSD Pages 레이어)는 **`src/pages`**에 있습니다.
|
||||
- Next.js 라우트는 **`app/`** 에서 `src/pages`를 re-export 하는 방식으로 연결됩니다.
|
||||
|
||||
[Feature-Sliced Design - Usage with Next.js (App Router)](https://feature-sliced.design/docs/guides/tech/with-nextjs)
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user