[as] 프로젝트 생성
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# apps/as/shared/config/env.ts 참고
|
||||
# src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
NEXT_PUBLIC_SERVER_API_URL=
|
||||
SERVER_API_URL=
|
||||
|
||||
# 401 시 브라우저 리다이렉트 대상
|
||||
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
|
||||
|
||||
const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
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 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,5 +0,0 @@
|
||||
# apps/as/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
NEXT_PUBLIC_SERVER_API_URL=
|
||||
+18
-73
@@ -1,4 +1,4 @@
|
||||
# AS Application
|
||||
# Template Application
|
||||
|
||||
Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처
|
||||
|
||||
@@ -18,7 +18,7 @@ Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처
|
||||
이 프로젝트는 **Feature-Sliced Design v2.1**과 **FSD 공식 Next.js 가이드**에 맞춰 구성되어 있습니다. Next.js `app/`은 라우트만 담당하고, 실제 레이어는 `src/` 아래에 있습니다.
|
||||
|
||||
```
|
||||
as/
|
||||
_template/
|
||||
├── app/ # Next.js App Router (루트) — 라우트·레이아웃 진입점만
|
||||
│ ├── layout.tsx # 루트 레이아웃 (src/app에서 providers, styles import)
|
||||
│ └── (with-base-layout)/
|
||||
@@ -112,43 +112,20 @@ shared/ ↓ 가장 낮은 책임, 의존성 없음
|
||||
- **특징**: 비즈니스 로직 없음, 슬라이스가 없고 세그먼트로만 구성
|
||||
- **의존성**: 외부 라이브러리만 사용 가능 (다른 레이어 불가)
|
||||
|
||||
## 레이어 간 Import 규칙
|
||||
|
||||
> **핵심 규칙**: 슬라이스 내의 모듈(파일)은 **오직 하위 레이어**에 있는 다른 슬라이스만 import할 수 있습니다.
|
||||
|
||||
예시:
|
||||
- `features/auth-login/ui/form.tsx`는 `entities/user`와 `shared/ui`를 import 가능
|
||||
- 하지만 다른 `features/post-create`나 상위 레이어인 `widgets`는 import 불가
|
||||
- 같은 슬라이스 내부(`features/auth-login/`)의 다른 파일들은 자유롭게 import 가능
|
||||
|
||||
## Public API 규칙
|
||||
|
||||
> **슬라이스 외부의 모듈은 슬라이스의 Public API만 참조할 수 있으며, 내부 파일 구조에 직접 접근할 수 없습니다.**
|
||||
|
||||
모든 슬라이스(및 슬라이스가 없는 레이어의 세그먼트)는 반드시 `index.ts`를 통해 Public API를 정의해야 합니다.
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import
|
||||
import { LoginForm } from 'features/auth-login';
|
||||
|
||||
// ❌ 잘못된 import (내부 구조 직접 접근)
|
||||
import { LoginForm } from 'features/auth-login/ui/login-form';
|
||||
```
|
||||
|
||||
## 시작하기
|
||||
|
||||
```bash
|
||||
# 의존성 설치
|
||||
npm install
|
||||
pnpm install
|
||||
|
||||
# 개발 서버 실행
|
||||
npm run dev
|
||||
pnpm run dev
|
||||
|
||||
# 프로덕션 빌드
|
||||
npm run build
|
||||
pnpm run build
|
||||
|
||||
# 프로덕션 서버 실행
|
||||
npm start
|
||||
pnpm start
|
||||
```
|
||||
|
||||
## 환경 변수
|
||||
@@ -168,16 +145,16 @@ NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
||||
```typescript
|
||||
// ✅ 올바른 import (하위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { User } from 'entities/user';
|
||||
import { Button } from 'shared/ui/button';
|
||||
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 { PostForm } from "features/post-create"; // 금지!
|
||||
|
||||
// ❌ 잘못된 import (상위 레이어)
|
||||
// features/auth-login/ui/form.tsx
|
||||
import { Header } from 'widgets/header'; // 금지!
|
||||
import { Header } from "widgets/header"; // 금지!
|
||||
```
|
||||
|
||||
### 2. Public API 규칙
|
||||
@@ -186,10 +163,10 @@ import { Header } from 'widgets/header'; // 금지!
|
||||
|
||||
```typescript
|
||||
// ✅ 올바른 import (Public API 사용)
|
||||
import { LoginForm } from 'features/auth-login';
|
||||
import { LoginForm } from "features/auth-login";
|
||||
|
||||
// ❌ 잘못된 import (내부 구조 직접 접근)
|
||||
import { LoginForm } from 'features/auth-login/ui/login-form';
|
||||
import { LoginForm } from "features/auth-login/ui/login-form";
|
||||
```
|
||||
|
||||
**Public API 작성 시 주의사항**:
|
||||
@@ -199,44 +176,12 @@ import { LoginForm } from 'features/auth-login/ui/login-form';
|
||||
|
||||
```typescript
|
||||
// ❌ 나쁜 예시
|
||||
export * from './ui/Comment';
|
||||
export * from './model/comments';
|
||||
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';
|
||||
```
|
||||
|
||||
### 3. 슬라이스 독립성
|
||||
|
||||
- **Zero Coupling**: 같은 레이어의 슬라이스들은 서로 독립적이어야 합니다
|
||||
- **High Cohesion**: 슬라이스는 주요 목표와 관련된 대부분의 코드를 포함해야 합니다
|
||||
- 예외: Entities 레이어에서 필요한 경우 `@x` 표기법으로 cross-import 가능
|
||||
|
||||
### 4. 세그먼트 명명 규칙
|
||||
|
||||
세그먼트 이름은 **목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다:
|
||||
|
||||
- ❌ **나쁜 이름**: `components`, `hooks`, `types` (무엇인지만 설명)
|
||||
- ✅ **좋은 이름**: `ui`, `api`, `model`, `lib` (왜 있는지 설명)
|
||||
|
||||
## 코드 규칙
|
||||
|
||||
- TypeScript 사용 (strict mode)
|
||||
- 함수형 컴포넌트 사용
|
||||
- Named exports 선호
|
||||
- Tailwind CSS 사용
|
||||
- ESLint + Prettier 적용
|
||||
- Public API는 명시적으로 정의 (와일드카드 export 지양)
|
||||
|
||||
## 아키텍처 린팅
|
||||
|
||||
코드베이스의 FSD 규칙 준수를 자동으로 검증하려면 [Steiger](https://github.com/feature-sliced/steiger)를 사용하세요.
|
||||
|
||||
```bash
|
||||
# Steiger 설치 및 실행
|
||||
npm install -D @feature-sliced/steiger
|
||||
npx steiger src
|
||||
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 +0,0 @@
|
||||
export { InstlSncnListPage as default } from "@/pages/instl-sncn";
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { BaseLayoutWrapper } from "@/app/layouts/base-layout";
|
||||
|
||||
interface WithBaseLayoutProps {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function WithBaseLayout({ children }: WithBaseLayoutProps) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { MainPage as default } from "@/pages/home";
|
||||
@@ -0,0 +1,94 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
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,
|
||||
};
|
||||
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 result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
|
||||
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,
|
||||
});
|
||||
|
||||
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||
const host = request.headers.get("host");
|
||||
const actualHost = forwardedHost || host || new URL(request.url).hostname.toString();
|
||||
|
||||
// 운영 환경에서 domain 설정
|
||||
const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost;
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
domain: domain,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
|
||||
} catch (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");
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginPage } from "@/pages/auth/login";
|
||||
export default LoginPage;
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { Providers } from "@/app/providers";
|
||||
import { setCookie } from "@/shared/lib";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
|
||||
import "@/app/styles/globals.css";
|
||||
import "@/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "행정지원시스템",
|
||||
@@ -15,8 +14,6 @@ interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
setCookie("site-id-key", SITE_ID);
|
||||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
const theme = (await cookies()).get("site-theme-key");
|
||||
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | -------------------------------------------------------- |
|
||||
| ---------- | -------------------------------------------- |
|
||||
| 버전 | 0.1 (초안) |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 결재선 관리 |
|
||||
| 화면 유형 | [x] 조회 [x] 입력 [x] 수정 [x] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 사이트관리 > 결재선 관리 |
|
||||
| 화면 유형 | [v] 조회 [v] 입력 [v] 수정 [v] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 디지털행정지원 > 시스템관리 > 결재선 관리 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
전자결재 처리에 사용할 **결재서식**(템플릿)을 등록·관리하고, 각 서식별로 **결재선**(접수 → 검토 → 최종승인 단계)을 구성한다. 좌측 상단의 결재서식 목록에서 하나를 선택하면 하단 영역에 해당 서식의 결재 단계(결재자/순서/구분)가 표시되며, 사용자는 결재자 추가·수정·삭제와 결재선 저장을 수행한다.
|
||||
전자결재 처리에 사용할 **결재서식**(템플릿)을 등록·관리하고, 각 서식별로 **결재선**(접수 -> 검토 -> 승인 -> 최종승인 단계)을 구성한다. 상단의 결재서식 목록에서 하나를 선택하면 하단 영역에 해당 서식의 결재 단계(결재자/순서/구분)가 표시되며, 사용자는 결재자 추가·수정·삭제와 결재선 저장을 수행한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -165,14 +165,14 @@ END
|
||||
### 4-1. 헤더 / 툴바
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | ------------------------------------------------------------- |
|
||||
| ----------- | ----------------------------------------------- |
|
||||
| 섹션 타이틀 | 전자결재선 관리 목록 |
|
||||
| 우측 버튼 | **[서식 등록]** — 결재서식 등록 다이얼로그 오픈 |
|
||||
|
||||
### 4-2. 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| ------------------ | -------------------- | ------ | ------------------------------------------------- |
|
||||
| ------------------ | ----------------- | ------ | --------------------------------------------- |
|
||||
| 전자결재서식아이디 | `elctAtrzTmpltId` | string | 예) `TMPL-0001` (서버 자동 발급, TBD) |
|
||||
| 결재서식명 | `atrzTmpltNm` | string | 예) `설치인가 검토 결재` |
|
||||
| 문서구분 | `docSeNm` | string | 예) `설치인가`, `기본계획` (코드/명 매핑 TBD) |
|
||||
@@ -185,7 +185,7 @@ END
|
||||
### 4-3. 버튼
|
||||
|
||||
| 버튼명 | 위치 | 동작 |
|
||||
| ---------- | ------------------ | -------------------------------------------------------------------- |
|
||||
| --------- | -------------- | ----------------------------------------------------------------- |
|
||||
| 서식 등록 | 섹션 우측 상단 | 결재서식 등록 다이얼로그 오픈 (입력 항목·필수값 TBD) |
|
||||
| 수정 | 행별 | 결재서식 수정 다이얼로그 오픈 (해당 행 값으로 초기화) |
|
||||
| 삭제 | 행별 | 확인 다이얼로그 후 삭제. 선택 중인 서식이면 영역 B 상태도 초기화. |
|
||||
@@ -193,7 +193,7 @@ END
|
||||
### 4-4. 다이얼로그(개요)
|
||||
|
||||
| 다이얼로그명 | 트리거 | 입력 항목(예상) | 비고 |
|
||||
| ---------------------- | ---------------- | ------------------------------------------------ | -------------------------- |
|
||||
| ------------------ | ---------------- | ------------------------------ | ------------------------ |
|
||||
| 결재서식 등록 | [서식 등록] 클릭 | 결재서식명, 문서구분(드롭다운) | 등록 성공 시 영역 A 갱신 |
|
||||
| 결재서식 수정 | 행별 [수정] | 결재서식명, 문서구분 | 기존 값으로 초기화 |
|
||||
| 결재서식 삭제 확인 | 행별 [삭제] | (확인/취소) | |
|
||||
@@ -207,7 +207,7 @@ END
|
||||
### 5-1. 헤더 / 상태 표시
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------ | ------------------------------------------------------------------------------------- |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| 섹션 타이틀 | 결재선 관리 |
|
||||
| 우측 라벨 | `선택된 서식: {전자결재서식아이디}` — 영역 A에서 선택한 서식 ID를 강조 색으로 표시. 미선택 시 영역 B 비활성. |
|
||||
|
||||
@@ -216,7 +216,7 @@ END
|
||||
#### 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | 데이터 소스 | API 파라미터 매핑 | 비고 |
|
||||
| -------------------- | -------------------- | ---- | ------------ | ---------------------- | ----------------- | --------------------------------------------------------------- |
|
||||
| ---------------------- | ----------------- | ---- | ----------- | -------------------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| 결재선아이디(자동발급) | 텍스트(읽기 전용) | — | `APLN-XXXX` | 서버 자동 발급 (TBD) | TBD | 입력 불가. 저장 시 서버 발급. 표시값은 미발급 자리표시(또는 임시 ID). |
|
||||
| 결재구분 | 드롭다운 | 예 | `접수` | 코드 (TBD) | TBD | 옵션: **접수 / 검토 / 최종승인** (그리드 행 기준) |
|
||||
| 결재자명 | 텍스트(읽기 전용) | 예 | `""` | [검토인력 선택] 결과 | TBD | 자유 입력 불가. 다이얼로그 선택 결과로 채움. |
|
||||
@@ -230,7 +230,7 @@ END
|
||||
#### 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| 검토인력 선택 | **검토인력 선택 다이얼로그** 오픈. 선택 시 `결재자명`과 내부 `결재자 ID`를 폼 상태에 반영. |
|
||||
| 결재자 추가 | 입력 폼 값을 영역 B-2 그리드에 **임시 행**으로 추가하고 폼을 초기화한다. (서버 호출 없음, 저장 시 일괄 반영.) |
|
||||
| 결재선 저장 | 영역 B-2의 결재선 목록을 일괄 저장 API로 전송한다. 성공 시 그리드 재조회 + 토스트 표시. |
|
||||
@@ -241,7 +241,7 @@ END
|
||||
#### 다이얼로그
|
||||
|
||||
| 다이얼로그명 | 트리거 | 표시 내용 / 입력 항목 | 결과 |
|
||||
| -------------------- | -------------------- | -------------------------------------------------------- | --------------------------------------------------- |
|
||||
| ---------------- | --------------- | ---------------------------------------------------------- | ----------------------------------- |
|
||||
| 검토인력 선택 | [검토인력 선택] | 사용자 검색 필터(이름/소속 등 TBD), 사용자 목록(단일 선택) | 선택된 사용자 정보를 입력 폼에 반영 |
|
||||
| 결재자 수정 | B-2 행별 [수정] | 결재구분, 결재자명(선택), 결재순서번호 | 확인 시 해당 행 갱신 |
|
||||
| 결재자 삭제 확인 | B-2 행별 [삭제] | (확인/취소) | 확인 시 해당 행 삭제 |
|
||||
@@ -251,7 +251,7 @@ END
|
||||
#### 그리드 컬럼
|
||||
|
||||
| 컬럼명 | Field | 타입 | 비고 |
|
||||
| -------------- | ---------------- | ------ | ---------------------------------------------------------- |
|
||||
| ------------ | ------------ | ------ | ------------------------------------- |
|
||||
| 결재선아이디 | `atrzLineId` | string | 예) `APLN-0001` (서버 자동 발급, TBD) |
|
||||
| 결재순서번호 | `atrzSeqNo` | number | 1부터 시작. 단계 순서. |
|
||||
| 결재구분 | `atrzSeNm` | string | `접수` / `검토` / `최종승인` |
|
||||
@@ -272,7 +272,7 @@ END
|
||||
### 6-1. 공통
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ----------- | ------------------------------------------------------------------- |
|
||||
| ----------- | ------------------------------------------- |
|
||||
| Base prefix | TBD (예: `/server` 또는 결재 서비스 prefix) |
|
||||
| 인증 | JWT 토큰 기반 (Authorization 헤더) |
|
||||
| 클라이언트 | TBD (예: `@/entities/approval-line`) |
|
||||
@@ -280,7 +280,7 @@ END
|
||||
### 6-2. 전자결재서식 (영역 A)
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
|
||||
| --------------------- | ------ | ---- | ----------- | ------------ | ---------------------- |
|
||||
| ------------------ | ------ | --- | ----------- | ------------ | ---------------------- |
|
||||
| 결재서식 목록 조회 | TBD | TBD | TBD | TBD | TBD |
|
||||
| 결재서식 등록 | TBD | TBD | TBD | TBD | TBD |
|
||||
| 결재서식 수정 | TBD | TBD | TBD | TBD | TBD |
|
||||
@@ -289,7 +289,7 @@ END
|
||||
### 6-3. 결재선 (영역 B)
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
|
||||
| ------------------------------- | ------ | ---- | ----------- | ------------ | ---------------------- |
|
||||
| ------------------------ | ------ | --- | ------------------------------------ | ------------ | ---------------------- |
|
||||
| 결재선 목록 조회(서식별) | TBD | TBD | `{ elctAtrzTmpltId }` (TBD) | TBD | TBD |
|
||||
| 결재선 추가/일괄 저장 | TBD | TBD | `{ elctAtrzTmpltId, lines[] }` (TBD) | TBD | TBD |
|
||||
| 결재선 수정 | TBD | TBD | TBD | TBD | TBD |
|
||||
@@ -298,7 +298,7 @@ END
|
||||
### 6-4. 검토인력 (다이얼로그)
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API(클라이언트 함수명) |
|
||||
| ------------------- | ------ | ---- | ----------- | ------------ | ---------------------- |
|
||||
| ------------------ | ------ | --- | --------------- | ------------ | ---------------------- |
|
||||
| 검토인력 목록 조회 | TBD | TBD | TBD (검색 필터) | TBD | TBD |
|
||||
|
||||
---
|
||||
@@ -316,7 +316,7 @@ END
|
||||
### 7-2. 모듈·파일 배치(제안)
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------- |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 라우트(App Router) | `apps/as/app/(with-base-layout)/approval-line-management/page.tsx` |
|
||||
| 페이지 컴포넌트 | `apps/as/src/pages/approval-line-management/index.ts`<br/>`.../ui/approval-line-management-page.tsx` |
|
||||
| 위젯(영역 단위) | `apps/as/src/widgets/approval-template-list/`<br/>`apps/as/src/widgets/approval-line-editor/` |
|
||||
@@ -328,7 +328,7 @@ END
|
||||
### 7-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 주요 UI | 비고 |
|
||||
| ----- | ----------------------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| A | 섹션 헤더 + Button(`서식 등록`) + DataTable (행 선택, 행별 Action 버튼) | 선택 행 강조, 행 클릭으로 영역 B 갱신 |
|
||||
| B 헤더 | 상태 라벨 `선택된 서식: {ID}` | 미선택 시 영역 B-1·B-2 비활성 |
|
||||
| B-1 | 읽기 전용 Input(아이디), Select(결재구분), Input+Button(결재자/검토인력 선택), NumberInput(순서번호), Action 버튼 3종(`결재자 추가`/`결재선 저장`/`초기화`) | 폼 유효성 검증 후 그리드에 임시 행 추가 |
|
||||
|
||||
+30
-15
@@ -1,18 +1,33 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import baseEsLintConfig from "../../eslint.config.mjs";
|
||||
|
||||
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",
|
||||
]),
|
||||
]);
|
||||
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,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { PORT, PROXY_HOST, SERVER_API_URL, SITE_ID } from "@/shared/config";
|
||||
|
||||
const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
const serverApiUrl = process.env.SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@faker-js/faker": "^10.3.0",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
@@ -26,9 +25,11 @@
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.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",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 이 폴더는 비워 둡니다 (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)
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -2,10 +2,12 @@
|
||||
|
||||
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";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { BaseLayout } from "@common/components";
|
||||
|
||||
interface BaseLayoutWrapperProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import { queryClient } from "@/shared/lib";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
|
||||
interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
@@ -11,10 +14,10 @@ interface ProvidersProps {
|
||||
export function Providers({ children }: ProvidersProps) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<div className="text-2xl">
|
||||
<StoreProvider>
|
||||
<AuthWrapper>{children}</AuthWrapper>
|
||||
</StoreProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
.card {
|
||||
position: relative;
|
||||
margin: 0.6rem;
|
||||
box-sizing: border-box;
|
||||
min-height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
font-size: 1.3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card_width10 {
|
||||
width: calc(10% - 12px);
|
||||
}
|
||||
|
||||
.card_width20 {
|
||||
width: calc(20% - 12px);
|
||||
}
|
||||
|
||||
.card_width25 {
|
||||
width: calc(25% - 12px);
|
||||
}
|
||||
|
||||
.card_width30 {
|
||||
width: calc(30% - 12px);
|
||||
}
|
||||
|
||||
.card_width33 {
|
||||
width: calc(33.333% - 12px);
|
||||
}
|
||||
|
||||
.card_width40 {
|
||||
width: calc(40% - 12px);
|
||||
}
|
||||
|
||||
.card_width45 {
|
||||
width: calc(45% - 12px);
|
||||
}
|
||||
|
||||
.card_width50 {
|
||||
width: calc(50% - 12px);
|
||||
}
|
||||
|
||||
.card_width60 {
|
||||
width: calc(60% - 12px);
|
||||
}
|
||||
|
||||
.card_width66 {
|
||||
width: calc(66.666% - 12px);
|
||||
}
|
||||
|
||||
.card_width70 {
|
||||
width: calc(70% - 12px);
|
||||
}
|
||||
|
||||
.card_width75 {
|
||||
width: calc(75% - 12px);
|
||||
}
|
||||
|
||||
.card_width80 {
|
||||
width: calc(80% - 12px);
|
||||
}
|
||||
|
||||
.card_width90 {
|
||||
width: calc(90% - 12px);
|
||||
}
|
||||
|
||||
.card_width100 {
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.card_nopadding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_nomargin {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.topline {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.bottomline {
|
||||
border-bottom: 1px solid #e6eef3;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-border {
|
||||
border: 1px solid #dbe2e6;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-top: -0.6rem;
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e1e7eb;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e1e7eb;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 블랙 테마 */
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
background-color: rgba(255, 255, 255, 0.065);
|
||||
border-color: #38383a;
|
||||
}
|
||||
.card-header {
|
||||
border-bottom-color: #3f4042;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
JWT_EXPIRATION_TIME,
|
||||
} from "@/shared/config";
|
||||
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
import type { ILogin, LoginService } from "../model/login-interface";
|
||||
|
||||
export class LoginApiService implements LoginService {
|
||||
private static instance: LoginApiService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): LoginApiService {
|
||||
if (!LoginApiService.instance) {
|
||||
LoginApiService.instance = new LoginApiService();
|
||||
}
|
||||
return LoginApiService.instance;
|
||||
}
|
||||
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("data ", data);
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
if (data.message == "Invalid Credentials") {
|
||||
throw new Error("아이디와 비밀번호가 일치하지 않습니다.");
|
||||
} else if (data.message == "User not found") {
|
||||
throw new Error("존재하지 않는 아이디입니다.");
|
||||
} else if (data.message == "User is not active") {
|
||||
throw new Error("활성화되지 않은 계정입니다.");
|
||||
} else if (data.message == "User is not verified") {
|
||||
throw new Error("이메일 인증이 필요합니다.");
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
}
|
||||
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setupRefreshTimer(() => this.silentRefresh(), JWT_EXPIRATION_TIME);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new Error(`Login 실패 ${error.response?.data?.message}`);
|
||||
}
|
||||
throw new Error(`Login 실패 ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async silentRefresh(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, {
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
} catch (error) {
|
||||
tokenManager.clearAuthToken();
|
||||
if (error instanceof AxiosError) {
|
||||
throw new Error(`Silent refresh failed: ${error.response?.data?.message}`);
|
||||
}
|
||||
throw new Error(`Silent refresh failed: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LoginApi = LoginApiService.getInstance();
|
||||
@@ -0,0 +1,2 @@
|
||||
export { LoginApi } from "./api/login-service";
|
||||
export { AuthWrapper } from "./lib/auth-wrapper";
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { client } from "@/shared/api";
|
||||
import { AUTH_LOGIN_REDIRECT_URL, AUTH_USER_ID, ENDPOINT } from "@/shared/config";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(`${ENDPOINT.USER_SERVICE.USERS}/info/me`);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { AuthWrapper };
|
||||
@@ -0,0 +1,82 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, removeCookie,setCookie } from "@/shared/lib";
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): TokenManager {
|
||||
if (!TokenManager.instance) {
|
||||
TokenManager.instance = new TokenManager();
|
||||
}
|
||||
return TokenManager.instance;
|
||||
}
|
||||
|
||||
public getToken(key: string): string | undefined {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
const domain = this.getCookieDomain();
|
||||
|
||||
setCookie(key, value, {
|
||||
secure: window.location.protocol === "https:",
|
||||
sameSite: "lax",
|
||||
domain: domain,
|
||||
path: "/", // 모든 경로에서 접근 가능
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private getCookieDomain(): string | undefined {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 개발 환경에서는 localhost 도메인 사용
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||
return undefined; // localhost에서는 도메인 설정하지 않음
|
||||
}
|
||||
|
||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
return ".hasuon.or.kr";
|
||||
}
|
||||
|
||||
// 기타 환경에서는 현재 호스트명 사용
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public removeToken(key: string): void {
|
||||
const domain = this.getCookieDomain();
|
||||
removeCookie(key, { domain, path: "/" });
|
||||
}
|
||||
|
||||
public setupRefreshTimer(
|
||||
callback: () => Promise<void>,
|
||||
expirationTime: number,
|
||||
): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
}
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
callback().catch((error) => {
|
||||
console.error("Silent refresh failed:", error);
|
||||
this.clearAuthToken();
|
||||
});
|
||||
}, expirationTime - 60000);
|
||||
}
|
||||
|
||||
public clearAuthToken(): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
this.removeToken(CLAIM_NAME);
|
||||
this.removeToken(AUTH_USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenManager = TokenManager.getInstance();
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface ILogin {
|
||||
userId: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginService {
|
||||
login(payload: ILogin): Promise<void>;
|
||||
silentRefresh(): Promise<void>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export { instlSncnService } from "./instl-sncn-service-impl";
|
||||
export {
|
||||
instlSncnKeys,
|
||||
useListPageLoadCnfm,
|
||||
useRegistPageLoadCnfm,
|
||||
useViewPageLoadCnfm,
|
||||
useSelectInstlCnfmListCnt,
|
||||
useSelectInstlCnfmList,
|
||||
useSelectInstlCnfmDtl,
|
||||
useInsertInstlCnfm,
|
||||
useMergeInstlCnfm,
|
||||
useDeleteInstlCnfm,
|
||||
useUpdatePrgrsSttusCnfm,
|
||||
useSaveInstlCnfmPrgrsStts,
|
||||
useSaveInstlCnfmSanctnPrgrsStts,
|
||||
useInsertInstlCnfmSpl,
|
||||
useSelectEssntlInstlCnfmAtchFileList,
|
||||
useSelectInstlCnfmExmntFile,
|
||||
useSelectInstlCnfmEtcFile,
|
||||
useSelectInstlCnfmSpl,
|
||||
useSelectInstlCnfmExmntResult,
|
||||
useSelectInstlCnfmOpinionList,
|
||||
} from "./queries";
|
||||
@@ -1,222 +0,0 @@
|
||||
import { apiClient } from "@/shared/api";
|
||||
import type {
|
||||
InstlSncnService,
|
||||
FnrscVO,
|
||||
ListPageResult,
|
||||
RegistPageData,
|
||||
ViewPageData,
|
||||
SplmntRqst,
|
||||
} from "../model";
|
||||
|
||||
class InstlSncnServiceImpl implements InstlSncnService {
|
||||
async selectInstlCnfmListCount(vo: FnrscVO): Promise<number> {
|
||||
const response = await apiClient.get<{ count: number }>(
|
||||
"/fnrsc/cnfm/list/count",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data.count;
|
||||
}
|
||||
|
||||
async selectInstlCnfmList(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>("/fnrsc/cnfm/list", {
|
||||
params: vo,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async listPageLoadCnfm(vo: FnrscVO): Promise<ListPageResult> {
|
||||
const response = await apiClient.get<ListPageResult>(
|
||||
"/fnrsc/cnfm/list/pageLoad.do",
|
||||
{
|
||||
params: {
|
||||
...vo,
|
||||
pageIndex: vo.pageIndex || 1,
|
||||
recordCountPerPage: vo.recordCountPerPage || 10,
|
||||
pageSize: vo.pageSize || 10,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async registPageLoadCnfm(vo: FnrscVO): Promise<RegistPageData> {
|
||||
const response = await apiClient.get<RegistPageData>(
|
||||
"/fnrsc/cnfm/regist/pageLoad.do",
|
||||
{
|
||||
params: vo,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async insertInstlCnfm(vo: FnrscVO, files?: File[]): Promise<void> {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(vo).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
if (typeof value === "object") {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (files && files.length > 0) {
|
||||
files.forEach((file) => {
|
||||
formData.append("uploadFile", file);
|
||||
});
|
||||
}
|
||||
|
||||
await apiClient.post("/fnrsc/cnfm/regist/insertFnrscCnfm.do", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async mergeInstlCnfm(vo: FnrscVO, files?: File[]): Promise<void> {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(vo).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
if (typeof value === "object") {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
} else {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (files && files.length > 0) {
|
||||
files.forEach((file) => {
|
||||
formData.append("uploadFile", file);
|
||||
});
|
||||
}
|
||||
|
||||
await apiClient.post("/fnrsc/cnfm/regist/mergeFnrscCnfm.do", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async selectInstlCnfmDetail(vo: FnrscVO): Promise<FnrscVO> {
|
||||
const response = await apiClient.get<FnrscVO>("/fnrsc/cnfm/detail", {
|
||||
params: vo,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async viewPageLoadCnfm(vo: FnrscVO): Promise<ViewPageData> {
|
||||
const response = await apiClient.get<ViewPageData>(
|
||||
"/fnrsc/cnfm/view/pageLoad.do",
|
||||
{
|
||||
params: vo,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateProgrsSttusCnfm(vo: FnrscVO): Promise<void> {
|
||||
await apiClient.post("/fnrsc/cnfm/update/updateProgrsSttus.do", vo);
|
||||
}
|
||||
|
||||
async saveInstlCnfmProgrsSttus(vo: FnrscVO, file?: File): Promise<void> {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(vo).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
if (file) {
|
||||
formData.append("uploadFile", file);
|
||||
}
|
||||
|
||||
await apiClient.post(
|
||||
"/fnrsc/cnfm/update/saveProgrsSttus.do",
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async saveInstlCnfmSanctnProgrsSttus(vo: FnrscVO): Promise<void> {
|
||||
await apiClient.post(
|
||||
"/fnrsc/cnfm/update/saveInstlCnfmSanctnProgrsSttus.do",
|
||||
vo
|
||||
);
|
||||
}
|
||||
|
||||
async deleteInstlCnfm(vo: FnrscVO): Promise<void> {
|
||||
await apiClient.post("/fnrsc/cnfm/regist/deleteInstlCnfm.do", vo);
|
||||
}
|
||||
|
||||
async selectEssntlInstlCnfmAtchFileList(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>(
|
||||
"/fnrsc/cnfm/files/essential",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async selectInstlCnfmExmntFile(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>(
|
||||
"/fnrsc/cnfm/files/examination",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async selectInstlCnfmEtcFile(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>("/fnrsc/cnfm/files/etc", {
|
||||
params: vo,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async selectInstlCnfmSanctnManage(
|
||||
vo: FnrscVO
|
||||
): Promise<Record<string, unknown>> {
|
||||
const response = await apiClient.get<Record<string, unknown>>(
|
||||
"/fnrsc/cnfm/sanction/manage",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async insertInstlCnfmSpl(vo: FnrscVO): Promise<void> {
|
||||
await apiClient.post("/fnrsc/cnfm/update/insertInstlCnfmSpl.do", vo);
|
||||
}
|
||||
|
||||
async selectInstlCnfmSpl(vo: FnrscVO): Promise<SplmntRqst | null> {
|
||||
const response = await apiClient.get<SplmntRqst | null>(
|
||||
"/fnrsc/cnfm/supplement",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async selectInstlCnfmExmntResult(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>(
|
||||
"/fnrsc/cnfm/examination/result",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async selectInstlCnfmOpinionList(vo: FnrscVO): Promise<FnrscVO[]> {
|
||||
const response = await apiClient.get<FnrscVO[]>(
|
||||
"/fnrsc/cnfm/opinion/list",
|
||||
{ params: vo }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const instlSncnService = new InstlSncnServiceImpl();
|
||||
@@ -1,312 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { instlSncnService } from "./instl-sncn-service-impl";
|
||||
import type { FnrscVO } from "../model";
|
||||
|
||||
export const instlSncnKeys = {
|
||||
all: ["instl-sncn"] as const,
|
||||
lists: () => [...instlSncnKeys.all, "list"] as const,
|
||||
list: (vo: FnrscVO) => [...instlSncnKeys.lists(), vo] as const,
|
||||
dtls: () => [...instlSncnKeys.all, "dtl"] as const,
|
||||
dtl: (instlCnfmDocNo: string) =>
|
||||
[...instlSncnKeys.dtls(), instlCnfmDocNo] as const,
|
||||
registPage: (jobSeSp: string) =>
|
||||
[...instlSncnKeys.all, "regist-page", jobSeSp] as const,
|
||||
viewPage: (instlCnfmDocNo: string) =>
|
||||
[...instlSncnKeys.all, "view-page", instlCnfmDocNo] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* 설치인가 목록 페이지 로드 (카운트 + 목록 + 페이지네이션)
|
||||
*/
|
||||
export function useListPageLoadCnfm(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.list(vo),
|
||||
queryFn: () => instlSncnService.listPageLoadCnfm(vo),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 등록 페이지 로드 (체크리스트 + 필수첨부파일)
|
||||
*/
|
||||
export function useRegistPageLoadCnfm(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.registPage(vo.jobSeSp || ""),
|
||||
queryFn: () => instlSncnService.registPageLoadCnfm(vo),
|
||||
enabled: !!vo.jobSeSp,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 상세/수정 페이지 로드
|
||||
*/
|
||||
export function useViewPageLoadCnfm(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.viewPage(vo.instlCnfmDocNo || ""),
|
||||
queryFn: () => instlSncnService.viewPageLoadCnfm(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 목록 카운트 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmListCnt(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [...instlSncnKeys.lists(), "count", vo],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmListCount(vo),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 목록 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmList(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.list(vo),
|
||||
queryFn: () => instlSncnService.selectInstlCnfmList(vo),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 상세 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmDtl(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: instlSncnKeys.dtl(vo.instlCnfmDocNo || ""),
|
||||
queryFn: () => instlSncnService.selectInstlCnfmDetail(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 신규 등록
|
||||
*/
|
||||
export function useInsertInstlCnfm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ vo, files }: { vo: FnrscVO; files?: File[] }) =>
|
||||
instlSncnService.insertInstlCnfm(vo, files),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 수정 (병합)
|
||||
*/
|
||||
export function useMergeInstlCnfm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ vo, files }: { vo: FnrscVO; files?: File[] }) =>
|
||||
instlSncnService.mergeInstlCnfm(vo, files),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
if (variables.vo.instlCnfmDocNo) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.dtl(variables.vo.instlCnfmDocNo),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.viewPage(variables.vo.instlCnfmDocNo),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 삭제
|
||||
*/
|
||||
export function useDeleteInstlCnfm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (vo: FnrscVO) => instlSncnService.deleteInstlCnfm(vo),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 진행상태 업데이트
|
||||
*/
|
||||
export function useUpdatePrgrsSttusCnfm() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (vo: FnrscVO) =>
|
||||
instlSncnService.updateProgrsSttusCnfm(vo),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
if (variables.instlCnfmDocNo) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.dtl(variables.instlCnfmDocNo),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.viewPage(variables.instlCnfmDocNo),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 진행상태 저장 (검토의견 + 파일)
|
||||
*/
|
||||
export function useSaveInstlCnfmPrgrsStts() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ vo, file }: { vo: FnrscVO; file?: File }) =>
|
||||
instlSncnService.saveInstlCnfmProgrsSttus(vo, file),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
if (variables.vo.instlCnfmDocNo) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.dtl(variables.vo.instlCnfmDocNo),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.viewPage(variables.vo.instlCnfmDocNo),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 기술검토 최종결재
|
||||
*/
|
||||
export function useSaveInstlCnfmSanctnPrgrsStts() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (vo: FnrscVO) =>
|
||||
instlSncnService.saveInstlCnfmSanctnProgrsSttus(vo),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
if (variables.instlCnfmDocNo) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.dtl(variables.instlCnfmDocNo),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.viewPage(variables.instlCnfmDocNo),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 보완요청 등록
|
||||
*/
|
||||
export function useInsertInstlCnfmSpl() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (vo: FnrscVO) => instlSncnService.insertInstlCnfmSpl(vo),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.lists(),
|
||||
});
|
||||
if (variables.instlCnfmDocNo) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.dtl(variables.instlCnfmDocNo),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: instlSncnKeys.viewPage(variables.instlCnfmDocNo),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 필수 첨부파일 목록 조회
|
||||
*/
|
||||
export function useSelectEssntlInstlCnfmAtchFileList(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [...instlSncnKeys.all, "essential-files", vo.instlCnfmDocNo],
|
||||
queryFn: () => instlSncnService.selectEssntlInstlCnfmAtchFileList(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 검토의견 첨부파일 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmExmntFile(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
...instlSncnKeys.all,
|
||||
"exmnt-files",
|
||||
vo.instlCnfmDocNo,
|
||||
vo.exmntOpinionSeqNo,
|
||||
],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmExmntFile(vo),
|
||||
enabled: !!vo.instlCnfmDocNo && !!vo.exmntOpinionSeqNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 기타 첨부파일 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmEtcFile(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [...instlSncnKeys.all, "etc-files", vo.instlCnfmDocNo],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmEtcFile(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 보완요청 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmSpl(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [...instlSncnKeys.all, "supplement", vo.instlCnfmDocNo],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmSpl(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 검토결과 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmExmntResult(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
...instlSncnKeys.all,
|
||||
"exmnt-result",
|
||||
vo.instlCnfmDocNo,
|
||||
vo.jobSe,
|
||||
],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmExmntResult(vo),
|
||||
enabled: !!vo.instlCnfmDocNo && !!vo.jobSe,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치인가 담당자 목록 조회
|
||||
*/
|
||||
export function useSelectInstlCnfmOpinionList(vo: FnrscVO) {
|
||||
return useQuery({
|
||||
queryKey: [...instlSncnKeys.all, "opinion-list", vo.instlCnfmDocNo],
|
||||
queryFn: () => instlSncnService.selectInstlCnfmOpinionList(vo),
|
||||
enabled: !!vo.instlCnfmDocNo,
|
||||
});
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
export type {
|
||||
PrgrsStts,
|
||||
FnrscVO,
|
||||
JobSeItem,
|
||||
CeckVO,
|
||||
AtchFile,
|
||||
ExmntOpinion,
|
||||
SplmntRqst,
|
||||
ListPageResult,
|
||||
RegistPageData,
|
||||
ViewPageData,
|
||||
InstlSncnService,
|
||||
} from "./model";
|
||||
|
||||
export {
|
||||
PRGRS_STTS_LABELS,
|
||||
PRGRS_STTS_OPTIONS,
|
||||
JOB_TYPE_CODE,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_RECORD_COUNT_PER_PAGE,
|
||||
} from "./model";
|
||||
|
||||
export {
|
||||
instlSncnService,
|
||||
instlSncnKeys,
|
||||
useListPageLoadCnfm,
|
||||
useRegistPageLoadCnfm,
|
||||
useViewPageLoadCnfm,
|
||||
useSelectInstlCnfmListCnt,
|
||||
useSelectInstlCnfmList,
|
||||
useSelectInstlCnfmDtl,
|
||||
useInsertInstlCnfm,
|
||||
useMergeInstlCnfm,
|
||||
useDeleteInstlCnfm,
|
||||
useUpdatePrgrsSttusCnfm,
|
||||
useSaveInstlCnfmPrgrsStts,
|
||||
useSaveInstlCnfmSanctnPrgrsStts,
|
||||
useInsertInstlCnfmSpl,
|
||||
useSelectEssntlInstlCnfmAtchFileList,
|
||||
useSelectInstlCnfmExmntFile,
|
||||
useSelectInstlCnfmEtcFile,
|
||||
useSelectInstlCnfmSpl,
|
||||
useSelectInstlCnfmExmntResult,
|
||||
useSelectInstlCnfmOpinionList,
|
||||
} from "./api";
|
||||
|
||||
export {
|
||||
fmtPrgrsStts,
|
||||
fmtJobSeComma,
|
||||
jobSeSpSummary,
|
||||
parseJobSeSpArr,
|
||||
fmtFileSize,
|
||||
fmtDt,
|
||||
vldtInstlSncnForm,
|
||||
canAply,
|
||||
canDel,
|
||||
canMdfy,
|
||||
canRqstSplmnt,
|
||||
canAprv,
|
||||
type VldtnErr,
|
||||
} from "./lib";
|
||||
@@ -1,47 +0,0 @@
|
||||
import { PRGRS_STTS_LABELS, type PrgrsStts } from "../model";
|
||||
|
||||
export function fmtPrgrsStts(stts: PrgrsStts): string {
|
||||
return PRGRS_STTS_LABELS[stts] || stts;
|
||||
}
|
||||
|
||||
export function fmtJobSeComma(jobSe: string): string {
|
||||
if (!jobSe) return "";
|
||||
return jobSe.split("").join(",");
|
||||
}
|
||||
|
||||
export function jobSeSpSummary(jobSeSp: string): string {
|
||||
if (!jobSeSp) return "";
|
||||
return jobSeSp.replace(/,/g, "");
|
||||
}
|
||||
|
||||
export function parseJobSeSpArr(jobSeSp: string): string[] {
|
||||
if (!jobSeSp) return [];
|
||||
return jobSeSp.split(",").filter(Boolean);
|
||||
}
|
||||
|
||||
export function fmtFileSize(bytes: number): string {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function fmtDt(dtString: string | undefined): string {
|
||||
if (!dtString) return "-";
|
||||
|
||||
try {
|
||||
const dt = new Date(dtString);
|
||||
return new Intl.DateTimeFormat("ko-KR", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(dt);
|
||||
} catch {
|
||||
return dtString;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export {
|
||||
fmtPrgrsStts,
|
||||
fmtJobSeComma,
|
||||
jobSeSpSummary,
|
||||
parseJobSeSpArr,
|
||||
fmtFileSize,
|
||||
fmtDt,
|
||||
} from "./format";
|
||||
|
||||
export {
|
||||
vldtInstlSncnForm,
|
||||
canAply,
|
||||
canDel,
|
||||
canMdfy,
|
||||
canRqstSplmnt,
|
||||
canAprv,
|
||||
type VldtnErr,
|
||||
} from "./validation";
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { FnrscVO } from "../model";
|
||||
|
||||
export interface VldtnErr {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function vldtInstlSncnForm(data: FnrscVO): VldtnErr[] {
|
||||
const errors: VldtnErr[] = [];
|
||||
|
||||
if (!data.jobSe) {
|
||||
errors.push({
|
||||
field: "jobSe",
|
||||
message: "업무구분을 선택해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.bplcNm || data.bplcNm.trim() === "") {
|
||||
errors.push({
|
||||
field: "bplcNm",
|
||||
message: "사업장명을 입력해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.fcltNm || data.fcltNm.trim() === "") {
|
||||
errors.push({
|
||||
field: "fcltNm",
|
||||
message: "시설명을 입력해주세요.",
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function canAply(data: FnrscVO): boolean {
|
||||
const errors = vldtInstlSncnForm(data);
|
||||
return errors.length === 0;
|
||||
}
|
||||
|
||||
export function canDel(
|
||||
prgrsStts: string | undefined,
|
||||
regId: string | undefined,
|
||||
currentUsrId: string
|
||||
): boolean {
|
||||
return prgrsStts === "SV" && regId === currentUsrId;
|
||||
}
|
||||
|
||||
export function canMdfy(
|
||||
prgrsStts: string | undefined,
|
||||
regId: string | undefined,
|
||||
currentUsrId: string
|
||||
): boolean {
|
||||
return (
|
||||
(prgrsStts === "SV" || prgrsStts === "SL") && regId === currentUsrId
|
||||
);
|
||||
}
|
||||
|
||||
export function canRqstSplmnt(
|
||||
prgrsStts: string | undefined,
|
||||
usrRole: string
|
||||
): boolean {
|
||||
return (
|
||||
(prgrsStts === "ER" || prgrsStts === "PR") &&
|
||||
(usrRole === "REVIEWER" || usrRole === "APPROVER")
|
||||
);
|
||||
}
|
||||
|
||||
export function canAprv(
|
||||
prgrsStts: string | undefined,
|
||||
usrRole: string
|
||||
): boolean {
|
||||
return prgrsStts === "PE" && usrRole === "APPROVER";
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { PrgrsStts } from "./types";
|
||||
|
||||
export const PRGRS_STTS_LABELS: Record<PrgrsStts, string> = {
|
||||
SV: "저장",
|
||||
ST: "신청",
|
||||
SL: "보완요청",
|
||||
ER: "기술검토중",
|
||||
EE: "기술검토완료",
|
||||
PR: "환경청검토중",
|
||||
PE: "환경청검토완료",
|
||||
AP: "승인",
|
||||
RJ: "반려",
|
||||
} as const;
|
||||
|
||||
export const PRGRS_STTS_OPTIONS = [
|
||||
{ value: "SV", label: "저장" },
|
||||
{ value: "ST", label: "신청" },
|
||||
{ value: "SL", label: "보완요청" },
|
||||
{ value: "ER", label: "기술검토중" },
|
||||
{ value: "EE", label: "기술검토완료" },
|
||||
{ value: "PR", label: "환경청검토중" },
|
||||
{ value: "PE", label: "환경청검토완료" },
|
||||
{ value: "AP", label: "승인" },
|
||||
{ value: "RJ", label: "반려" },
|
||||
] as const;
|
||||
|
||||
export const JOB_TYPE_CODE = "FNR01" as const;
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 10 as const;
|
||||
export const DEFAULT_RECORD_COUNT_PER_PAGE = 10 as const;
|
||||
@@ -1,22 +0,0 @@
|
||||
export type {
|
||||
PrgrsStts,
|
||||
FnrscVO,
|
||||
JobSeItem,
|
||||
CeckVO,
|
||||
AtchFile,
|
||||
ExmntOpinion,
|
||||
SplmntRqst,
|
||||
ListPageResult,
|
||||
RegistPageData,
|
||||
ViewPageData,
|
||||
} from "./types";
|
||||
|
||||
export {
|
||||
PRGRS_STTS_LABELS,
|
||||
PRGRS_STTS_OPTIONS,
|
||||
JOB_TYPE_CODE,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_RECORD_COUNT_PER_PAGE,
|
||||
} from "./constants";
|
||||
|
||||
export type { InstlSncnService } from "./instl-sncn-service";
|
||||
@@ -1,151 +0,0 @@
|
||||
import type {
|
||||
FnrscVO,
|
||||
CeckVO,
|
||||
AtchFile,
|
||||
SplmntRqst,
|
||||
ListPageResult,
|
||||
RegistPageData,
|
||||
ViewPageData,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 설치인가 서비스 인터페이스
|
||||
* Java CnfmService.java의 TypeScript 버전
|
||||
*/
|
||||
export interface InstlSncnService {
|
||||
/**
|
||||
* 설치인가 목록 카운트 조회
|
||||
* @param vo 검색 조건
|
||||
* @returns 목록 개수
|
||||
*/
|
||||
selectInstlCnfmListCount(vo: FnrscVO): Promise<number>;
|
||||
|
||||
/**
|
||||
* 설치인가 목록 조회
|
||||
* @param vo 검색 조건
|
||||
* @returns 설치인가 목록
|
||||
*/
|
||||
selectInstlCnfmList(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
|
||||
/**
|
||||
* 설치인가 목록 페이지 로드 (카운트 + 목록 + 페이지네이션)
|
||||
* @param vo 검색 조건
|
||||
* @returns 목록 페이지 데이터
|
||||
*/
|
||||
listPageLoadCnfm(vo: FnrscVO): Promise<ListPageResult>;
|
||||
|
||||
/**
|
||||
* 설치인가 등록 페이지 로드 (체크리스트 + 필수첨부파일)
|
||||
* @param vo jobSeSp 포함
|
||||
* @returns 등록 페이지 데이터
|
||||
*/
|
||||
registPageLoadCnfm(vo: FnrscVO): Promise<RegistPageData>;
|
||||
|
||||
/**
|
||||
* 설치인가 데이터 저장 (신규)
|
||||
* @param vo 설치인가 VO
|
||||
* @param files 첨부파일 목록
|
||||
*/
|
||||
insertInstlCnfm(vo: FnrscVO, files?: File[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 데이터 병합 (수정)
|
||||
* @param vo 설치인가 VO
|
||||
* @param files 첨부파일 목록
|
||||
*/
|
||||
mergeInstlCnfm(vo: FnrscVO, files?: File[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 상세 데이터 조회
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 설치인가 상세 정보
|
||||
*/
|
||||
selectInstlCnfmDetail(vo: FnrscVO): Promise<FnrscVO>;
|
||||
|
||||
/**
|
||||
* 설치인가 상세/수정 페이지 로드
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 상세 페이지 데이터
|
||||
*/
|
||||
viewPageLoadCnfm(vo: FnrscVO): Promise<ViewPageData>;
|
||||
|
||||
/**
|
||||
* 설치인가 진행상태 업데이트
|
||||
* @param vo instlCnfmDocNo, progrsSttus 포함
|
||||
*/
|
||||
updateProgrsSttusCnfm(vo: FnrscVO): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 진행상태 저장 (검토의견 + 파일)
|
||||
* @param vo instlCnfmDocNo, progrsSttus, exmntOpinionCn 포함
|
||||
* @param file 첨부파일
|
||||
*/
|
||||
saveInstlCnfmProgrsSttus(vo: FnrscVO, file?: File): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 기술검토 최종결재
|
||||
* @param vo instlCnfmDocNo, progrsSttus 포함
|
||||
*/
|
||||
saveInstlCnfmSanctnProgrsSttus(vo: FnrscVO): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 삭제
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
*/
|
||||
deleteInstlCnfm(vo: FnrscVO): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 신청 첨부파일 목록
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 필수 첨부파일 목록
|
||||
*/
|
||||
selectEssntlInstlCnfmAtchFileList(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
|
||||
/**
|
||||
* 설치인가 의견 첨부파일
|
||||
* @param vo instlCnfmDocNo, exmntOpinionSeqNo 포함
|
||||
* @returns 검토의견 첨부파일 목록
|
||||
*/
|
||||
selectInstlCnfmExmntFile(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
|
||||
/**
|
||||
* 설치인가 기타 첨부파일
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 기타 첨부파일 목록
|
||||
*/
|
||||
selectInstlCnfmEtcFile(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
|
||||
/**
|
||||
* 설치인가 결재권자 조회
|
||||
* @param vo instlCnfmDocNo, regId 포함
|
||||
* @returns 결재권자 정보
|
||||
*/
|
||||
selectInstlCnfmSanctnManage(vo: FnrscVO): Promise<Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* 설치인가 보완요청 등록
|
||||
* @param vo instlCnfmDocNo, splCn 포함
|
||||
*/
|
||||
insertInstlCnfmSpl(vo: FnrscVO): Promise<void>;
|
||||
|
||||
/**
|
||||
* 설치인가 보완요청 조회
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 보완요청 정보
|
||||
*/
|
||||
selectInstlCnfmSpl(vo: FnrscVO): Promise<SplmntRqst | null>;
|
||||
|
||||
/**
|
||||
* 설치인가 검토결과 조회
|
||||
* @param vo instlCnfmDocNo, jobSe 포함
|
||||
* @returns 검토결과 목록
|
||||
*/
|
||||
selectInstlCnfmExmntResult(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
|
||||
/**
|
||||
* 설치인가 담당자 정보 조회
|
||||
* @param vo instlCnfmDocNo 포함
|
||||
* @returns 담당자 목록
|
||||
*/
|
||||
selectInstlCnfmOpinionList(vo: FnrscVO): Promise<FnrscVO[]>;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
export type PrgrsStts =
|
||||
| "SV" // 저장
|
||||
| "ST" // 신청
|
||||
| "SL" // 보완요청
|
||||
| "ER" // 기술검토
|
||||
| "EE" // 기술검토완료
|
||||
| "PR" // 환경청검토
|
||||
| "PE" // 환경청검토완료
|
||||
| "AP" // 승인
|
||||
| "RJ"; // 반려
|
||||
|
||||
export interface FnrscVO {
|
||||
fnrscDscssDocNo?: string;
|
||||
instlCnfmDocNo?: string;
|
||||
seqNo?: string;
|
||||
yr?: string;
|
||||
bzCd?: string;
|
||||
bzNm?: string;
|
||||
strtYr?: string;
|
||||
edYr?: string;
|
||||
dtlBzCd?: string;
|
||||
untBzCd?: string;
|
||||
rgnCd?: string;
|
||||
rgnNm?: string;
|
||||
rvbsnCd?: string;
|
||||
adstDstrCd?: string;
|
||||
adstDstrNm?: string;
|
||||
bzSpec?: string;
|
||||
aidRt?: string;
|
||||
confBzCd?: string;
|
||||
bzPd?: string;
|
||||
bzCn?: string;
|
||||
jobSe?: string;
|
||||
jobSeSp?: string;
|
||||
jobSeList?: Array<JobSeItem>;
|
||||
jobSeNm?: string;
|
||||
fcltyKnd?: string;
|
||||
reusePrpos?: string;
|
||||
progrsSttus?: PrgrsStts;
|
||||
fileSeq?: number;
|
||||
regId?: string;
|
||||
regDt?: string;
|
||||
udtId?: string;
|
||||
udtDt?: string;
|
||||
registMan?: string;
|
||||
updtMan?: string;
|
||||
registDe?: string;
|
||||
updtDe?: string;
|
||||
atchFileId?: string;
|
||||
grpCd?: string;
|
||||
bplcNm?: string;
|
||||
fcltNm?: string;
|
||||
lclgvrnmntNm?: string;
|
||||
localCd?: string;
|
||||
exmntOpinionSeqNo?: string;
|
||||
exmntOpinionCn?: string;
|
||||
scdExmntOpinionSeqNo?: string;
|
||||
prmyExmntOpinionSeqNo?: string;
|
||||
splCn?: string;
|
||||
pageIndex?: number;
|
||||
recordCountPerPage?: number;
|
||||
pageSize?: number;
|
||||
searchKeyword?: string;
|
||||
searchCondition?: string;
|
||||
}
|
||||
|
||||
export interface JobSeItem {
|
||||
jobSeCd: string;
|
||||
jobSeNm: string;
|
||||
}
|
||||
|
||||
export interface CeckVO {
|
||||
ceckSn: string;
|
||||
ceckCn: string;
|
||||
ceckResultAt?: "Y" | "N";
|
||||
ceckResultCn?: string;
|
||||
jobSe: string;
|
||||
}
|
||||
|
||||
export interface AtchFile {
|
||||
atchFileId: string;
|
||||
fileSn: string;
|
||||
fileStreCours: string;
|
||||
streFileNm: string;
|
||||
orignlFileNm: string;
|
||||
fileExtsn: string;
|
||||
fileMg: number;
|
||||
fileType?: string;
|
||||
essntlAt?: "Y" | "N";
|
||||
}
|
||||
|
||||
export interface ExmntOpinion {
|
||||
exmntOpinionSeqNo: string;
|
||||
instlCnfmDocNo: string;
|
||||
exmntOpinionCn: string;
|
||||
exmntResultAt?: "Y" | "N";
|
||||
atchFileList?: AtchFile[];
|
||||
regId?: string;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
export interface SplmntRqst {
|
||||
splSeqNo: string;
|
||||
instlCnfmDocNo: string;
|
||||
splCn: string;
|
||||
splDt?: string;
|
||||
regId?: string;
|
||||
regDt?: string;
|
||||
}
|
||||
|
||||
export interface ListPageResult {
|
||||
cnt: number;
|
||||
list: FnrscVO[];
|
||||
pageIndex: number;
|
||||
lastPage: number;
|
||||
paginationInfo: {
|
||||
currentPageNo: number;
|
||||
recordCountPerPage: number;
|
||||
pageSize: number;
|
||||
totalRecordCount: number;
|
||||
lastPageNo: number;
|
||||
};
|
||||
searchVO: FnrscVO;
|
||||
}
|
||||
|
||||
export interface RegistPageData {
|
||||
ceckEssntlArr: Array<{
|
||||
jobSeCd: string;
|
||||
jobSeNm: string;
|
||||
ceckForm1: CeckVO[];
|
||||
essntlList: FnrscVO[];
|
||||
}>;
|
||||
jobSe: string;
|
||||
jobSeSp: string;
|
||||
grpCd: string;
|
||||
adstDstrCd: string;
|
||||
lclgvrnmntNm: string;
|
||||
localCd: string;
|
||||
}
|
||||
|
||||
export interface ViewPageData {
|
||||
view: FnrscVO;
|
||||
jobSeSp: string;
|
||||
ceckEssntlArr: Array<{
|
||||
jobSeCd: string;
|
||||
jobSeNm: string;
|
||||
ceckForm1: FnrscVO[];
|
||||
essntlList: FnrscVO[];
|
||||
}>;
|
||||
prmyFileList: FnrscVO[];
|
||||
scdFileList: FnrscVO[];
|
||||
etcFileList: FnrscVO[];
|
||||
grpCd: string;
|
||||
usrId: string;
|
||||
sanctnChk: Record<string, unknown>;
|
||||
spl: SplmntRqst | null;
|
||||
opinionList: FnrscVO[];
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export { type MenuItem } from "./model";
|
||||
export { type MenuItem } from "./model/types";
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { type MenuItem } from "./types";
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import { images } from "@common/assets";
|
||||
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage(""); // 에러 메시지 초기화
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
try {
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/";
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "로그인에 실패했습니다.";
|
||||
setErrorMessage(message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form id="loginForm" name="loginForm" method="post" onSubmit={handleSubmit}>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input type="checkbox" name="checkId" title="로그인ID 저장여부" id="checkId" />
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className="error_message">
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do" target="_blank">
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do" target="_blank">
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do" target="_blank">
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>주소 : (22689) 인천광역시 서구 환경로 42(경서동 종합환경연구단지)</span>
|
||||
<span>전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723, 3722</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
Executable
+325
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* OLYM Cascading Style Sheets
|
||||
* File Name : login.css
|
||||
* Description : 로그인 스타일시트
|
||||
* Author : Web business Team / Choi Hyun Mi
|
||||
* Email : desingtj@olym.co.kr
|
||||
* Date : 2023.07.20
|
||||
* Update : 2023.07.26
|
||||
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.fx {
|
||||
display: flex;
|
||||
-ms-display: flex;
|
||||
-webkit-display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.login .left_box {
|
||||
width: 100%;
|
||||
background: url(../../../../../public/images/bg_login.jpg) no-repeat
|
||||
center/cover;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 7% 5% 5% 5%;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 53px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .left_box .text h1 span {
|
||||
font-weight: 800;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .login_box {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 4% 12% 4% 6%;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2640a5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st {
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box > ul > li .input_st .diveicon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 28px;
|
||||
transform: translateY(-50%);
|
||||
font-family: "Xeicon";
|
||||
font-size: 25px;
|
||||
color: #003288;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
width: 100%;
|
||||
padding: 20px 20px 20px 70px;
|
||||
background: #f7f7f7;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
height: auto;
|
||||
}
|
||||
.login .login_box > ul > li input:focus {
|
||||
outline: 1px solid #3c5fe5;
|
||||
}
|
||||
.login .login_box > ul > li input::placeholder {
|
||||
color: #afabab;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 50px 0 45px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message i {
|
||||
color: #327af5;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box .error_message {
|
||||
margin: 20px 0;
|
||||
padding: 12px 20px;
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 5px;
|
||||
color: #c33;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.login .login_box .error_message span {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
padding: 24px 15px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-family: "Montserrat";
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 22px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login .login_box .login_btn:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.login .login_box .login_btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.login .footer .copy {
|
||||
margin-top: 36px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 3%;
|
||||
}
|
||||
.login .footer .copy p {
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
width: 44%;
|
||||
text-align: center;
|
||||
}
|
||||
.login .footer .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
content: "";
|
||||
width: 20%;
|
||||
height: 1px;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.login .footer .fot_btn {
|
||||
gap: 20px;
|
||||
margin: 30px 0 10px 0;
|
||||
}
|
||||
.login .footer .fot_btn a {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login .footer .fot_btn a:hover {
|
||||
color: #003288;
|
||||
}
|
||||
.login .footer .addr {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
gap: 3px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 비밀번호 변경 팝업창 */
|
||||
.login .modal .modal_container {
|
||||
width: 750px;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #666;
|
||||
}
|
||||
.login .pw_desc p {
|
||||
font-size: 1.25rem;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login .pw_desc span {
|
||||
color: #f60;
|
||||
font-weight: 700;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 50px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.login {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
.login .left_box {
|
||||
padding: 30px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 30px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .left_box .img img {
|
||||
height: 40px;
|
||||
}
|
||||
.login .login_box {
|
||||
padding: 30px;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
gap: 20px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st::after {
|
||||
left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
padding: 15px 10px 15px 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .id_save label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
padding: 15px;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 18px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.login .login_box .copy p {
|
||||
width: 100%;
|
||||
}
|
||||
.login .login_box .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
display: none;
|
||||
}
|
||||
.login .footer .addr {
|
||||
font-size: 11px;
|
||||
gap: 3px 10px;
|
||||
}
|
||||
.login .modal .modal_container {
|
||||
width: 90%;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
/* // login */
|
||||
@@ -0,0 +1 @@
|
||||
export { MainPage } from "./ui/main-page";
|
||||
@@ -0,0 +1,9 @@
|
||||
"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 +0,0 @@
|
||||
export { InstlSncnListPage } from "./ui/list-page";
|
||||
@@ -1,80 +0,0 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { FnrscVO } from "@/entities/instl-sncn";
|
||||
import { Badge } from "@common/components";
|
||||
|
||||
const columnHelper = createColumnHelper<FnrscVO>();
|
||||
|
||||
const LIST_STEPS = ["저장", "신청완료", "기술검토", "검토결과 회신"] as const;
|
||||
|
||||
function stepIndex(status: string | undefined): number {
|
||||
if (!status) return 0;
|
||||
const map: Record<string, number> = {
|
||||
SV: 0,
|
||||
ST: 1,
|
||||
LE: 2,
|
||||
SL: 2,
|
||||
ER: 2,
|
||||
TE: 2,
|
||||
TR: 2,
|
||||
TP: 2,
|
||||
EE: 2,
|
||||
TA: 3,
|
||||
};
|
||||
return map[status] ?? 0;
|
||||
}
|
||||
|
||||
function ProgrsSttusCell({ value }: { value?: string }) {
|
||||
const current = stepIndex(value);
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{LIST_STEPS.map((label, i) => (
|
||||
<>
|
||||
<Badge key={label} size="lg" color={i === current ? "blue" : "lightGray"}>
|
||||
{label}
|
||||
</Badge>
|
||||
{i < LIST_STEPS.length - 1 && (
|
||||
<i className="diveicon di-arrow-forward text-sm text-gray-400" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const instlSncnListColumns = [
|
||||
columnHelper.accessor("adstDstrNm", {
|
||||
id: "adstDstrNm",
|
||||
header: "지자체",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
columnHelper.accessor("bzNm", {
|
||||
id: "bzNm",
|
||||
header: "사업명",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
columnHelper.accessor("jobSeNm", {
|
||||
id: "jobSeNm",
|
||||
header: "업무구분",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
columnHelper.accessor("regId", {
|
||||
id: "regId",
|
||||
header: "담당자",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
columnHelper.accessor("rgnNm", {
|
||||
id: "rgnNm",
|
||||
header: "해당기관",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "progrsSttus",
|
||||
header: "진행상황",
|
||||
cell: (info) => <ProgrsSttusCell value={info.row.original.progrsSttus} />,
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
id: "regDt",
|
||||
header: "시행일",
|
||||
cell: (info) => info.getValue() ?? "-",
|
||||
}),
|
||||
];
|
||||
@@ -1,178 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { fakerKO as faker } from "@faker-js/faker";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useListPageLoadCnfm } from "@/entities/instl-sncn";
|
||||
import type { FnrscVO } from "@/entities/instl-sncn";
|
||||
import { instlSncnListColumns } from "../model/table-columns";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const JOB_SE_OPTIONS = [
|
||||
{ value: "ALL", label: "전체" },
|
||||
{ value: "SF", label: "하수처리시설" },
|
||||
{ value: "SP", label: "하수관로" },
|
||||
{ value: "RE", label: "재이용수" },
|
||||
] as const;
|
||||
|
||||
function buildMockFnrscVO(): FnrscVO {
|
||||
return {
|
||||
instlCnfmDocNo: faker.string.alphanumeric(10),
|
||||
adstDstrNm: faker.location.city(),
|
||||
bzNm: faker.company.name(),
|
||||
jobSeNm: faker.helpers.arrayElement(["하수처리시설", "하수관로", "재이용수"]),
|
||||
regId: faker.person.fullName(),
|
||||
rgnNm: faker.company.name(),
|
||||
progrsSttus: faker.helpers.arrayElement(["SV", "ST", "SL", "ER", "EE", "PR", "PE", "AP", "RJ"]),
|
||||
regDt: faker.date.past().toISOString().slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
const MOCK_LIST: FnrscVO[] = Array.from({ length: PAGE_SIZE }, () => buildMockFnrscVO());
|
||||
|
||||
export function InstlSncnListPage() {
|
||||
const router = useRouter();
|
||||
const [bzNm, setBzNm] = useState("");
|
||||
const [jobSeNm, setJobSeNm] = useState<string>("ALL");
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE });
|
||||
|
||||
const searchVO: FnrscVO = {
|
||||
bzNm: bzNm.trim() || undefined,
|
||||
jobSeNm: jobSeNm === "ALL" ? undefined : jobSeNm,
|
||||
pageIndex: pagination.pageIndex + 1,
|
||||
recordCountPerPage: PAGE_SIZE,
|
||||
pageSize: PAGE_SIZE,
|
||||
};
|
||||
|
||||
const { data, isPending, isError } = useListPageLoadCnfm(searchVO);
|
||||
const list = data?.list && data.list.length > 0 ? data.list : MOCK_LIST;
|
||||
const totalCount = data?.cnt ?? list.length;
|
||||
const pageCount = data?.paginationInfo?.lastPageNo ?? 1;
|
||||
|
||||
const handleSearch = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}, []);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(row: FnrscVO) => {
|
||||
if (row.instlCnfmDocNo) {
|
||||
router.push(`/instl-sncn/${row.instlCnfmDocNo}`);
|
||||
}
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const handleRegist = useCallback(() => {
|
||||
router.push("/instl-sncn/regist");
|
||||
}, [router]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: list,
|
||||
columns: instlSncnListColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
});
|
||||
|
||||
const isEmpty = list.length === 0;
|
||||
|
||||
return (
|
||||
<div className="contents px-[1.8rem] pb-8">
|
||||
<form
|
||||
onSubmit={handleSearch}
|
||||
className="card card-border flex flex-wrap items-center space-x-6 !p-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="bzNm" className="shrink-0 text-label">
|
||||
사업명
|
||||
</Label>
|
||||
<Input
|
||||
id="bzNm"
|
||||
placeholder="사업명"
|
||||
value={bzNm}
|
||||
onChange={(e) => setBzNm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch(e as unknown as React.FormEvent)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="jobSeNm" className="shrink-0 text-label">
|
||||
구분
|
||||
</Label>
|
||||
<Select
|
||||
value={jobSeNm}
|
||||
onValueChange={(v) => {
|
||||
setJobSeNm(v);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="jobSeNm" className="w-[12rem]">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{JOB_SE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="submit">조회</Button>
|
||||
</form>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mb-3 flex items-end justify-between">
|
||||
<p className="text-xl text-label">
|
||||
검색된 리스트 (<span className="font-semibold text-primary">{totalCount}</span>)
|
||||
</p>
|
||||
<Button type="button" color="green" onClick={handleRegist}>
|
||||
신청
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border bg-card">
|
||||
{isPending && (
|
||||
<div className="flex w-full items-center justify-center py-10">
|
||||
<div className="relative flex size-[4rem] items-center justify-center">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* {isError && (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-destructive">
|
||||
목록을 불러오는 중 오류가 발생했습니다.
|
||||
</div>
|
||||
)} */}
|
||||
{!isPending && !isError && isEmpty && (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
목록이 존재하지 않습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isPending && !isEmpty && (
|
||||
<div className="pb-10">
|
||||
<DataTable table={table} viewPagination onRowClick={handleRowClick} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PROXY_HOST, SITE_ID } from "@/shared/config";
|
||||
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({
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { apiClient } from "./client";
|
||||
export { client } from "./client";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const ENDPOINT = {
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
},
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
@@ -1,11 +1,10 @@
|
||||
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
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 AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
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 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 +1,2 @@
|
||||
export * from "./api";
|
||||
export * from "./env";
|
||||
|
||||
@@ -7,14 +7,21 @@ export function getCookie(name: string): string | undefined {
|
||||
return Cookies.get(name);
|
||||
}
|
||||
|
||||
export function setCookie(name: string, value: string, options?: Cookies.CookieAttributes): void {
|
||||
export function setCookie(
|
||||
name: string,
|
||||
value: string,
|
||||
options?: Cookies.CookieAttributes,
|
||||
): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
Cookies.set(name, value, options);
|
||||
}
|
||||
|
||||
export function removeCookie(name: string, options?: Cookies.CookieAttributes): void {
|
||||
export function removeCookie(
|
||||
name: string,
|
||||
options?: Cookies.CookieAttributes,
|
||||
): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import "dayjs/locale/ko";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { queryClient } from "./query-client";
|
||||
export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { dayjs } from "./dayjs";
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { queryClient } from "./query-client";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { useStore } from "zustand";
|
||||
|
||||
import { StoreContext } from "../providers";
|
||||
import type { UserStore } from "../store";
|
||||
|
||||
export const useUserStore = <T>(selector: (store: UserStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useUserStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.userStore, selector);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { StoreContext, StoreProvider } from "./store-provider";
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
|
||||
import { createUserStore } from "../store";
|
||||
|
||||
type StoreContextType = {
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
export const StoreContext = createContext<StoreContextType | undefined>(undefined);
|
||||
|
||||
export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const storeRef = useRef<StoreContextType | null>(null);
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
|
||||
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { createUserStore, type IUserInfo, type UserStore } from "./user-store";
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
export interface IUserInfo {
|
||||
regDt: string;
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
pswdAns: string;
|
||||
mphn: string;
|
||||
telno: string;
|
||||
zip: string;
|
||||
addr: string;
|
||||
daddr: string;
|
||||
laeSeCd: string;
|
||||
ogdpSeCd: string;
|
||||
aprvYn: string;
|
||||
autzrId: string;
|
||||
expryYmd: string;
|
||||
lastPswdChgDt: string;
|
||||
googleId: string;
|
||||
kakaoId: string;
|
||||
naverId: string;
|
||||
isSocialUser: boolean;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
export type UserState = {
|
||||
userInfo: IUserInfo | null;
|
||||
};
|
||||
|
||||
export type UserActions = {
|
||||
setUserInfo: (userInfo: IUserInfo | null) => void;
|
||||
};
|
||||
|
||||
export type UserStore = UserState & UserActions;
|
||||
|
||||
export const defaultInitState: UserState = {
|
||||
userInfo: null,
|
||||
};
|
||||
|
||||
export const createUserStore = (initState: UserState = defaultInitState) => {
|
||||
return createStore<UserStore>()((set) => ({
|
||||
...initState,
|
||||
setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })),
|
||||
}));
|
||||
};
|
||||
@@ -4,8 +4,12 @@ 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}",
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../../packages/common/components/dist/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@common/components": ["../../packages/common/components/dist/"],
|
||||
|
||||
Generated
+11
-50
@@ -138,9 +138,6 @@ importers:
|
||||
'@common/network':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/common/network
|
||||
'@faker-js/faker':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0
|
||||
'@hookform/resolvers':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.3(react-hook-form@7.72.0(react@19.2.4))
|
||||
@@ -172,6 +169,9 @@ importers:
|
||||
specifier: ^3.24.2
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3
|
||||
version: 3.3.5
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
@@ -179,8 +179,11 @@ importers:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 15.4.0
|
||||
version: 15.4.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
specifier: 15.3.8
|
||||
version: 15.3.8(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.0.0
|
||||
version: 5.2.0(eslint@9.39.4(jiti@2.6.1))
|
||||
postcss:
|
||||
specifier: ^8.4.49
|
||||
version: 8.5.8
|
||||
@@ -2497,10 +2500,6 @@ packages:
|
||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@faker-js/faker@10.3.0':
|
||||
resolution: {integrity: sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
|
||||
|
||||
'@faker-js/faker@9.9.0':
|
||||
resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
|
||||
@@ -2796,9 +2795,6 @@ packages:
|
||||
'@next/eslint-plugin-next@15.3.8':
|
||||
resolution: {integrity: sha512-CKpvDZhNXPUjUHmjHddcsqjwj46oQblFms2hr2uA79C0kVTiSXQEQxkawUsDNSofHhTI31IbSeBdVyuk/U1nKw==}
|
||||
|
||||
'@next/eslint-plugin-next@15.4.0':
|
||||
resolution: {integrity: sha512-dlGuj2B76ltswtpdDFQJfTlu6ux0v+EUYzsAYR1IqxqHi5iMPWhPRztgu8eqQZo4uNYkABKzLCmVeGDwbTYTiw==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -5032,15 +5028,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
eslint-config-next@15.4.0:
|
||||
resolution: {integrity: sha512-OFkQ8br0wuZNRlXDyLpfaCwzW+qayzNaiHNcLHDMW874/1Nktb2+cCUMlItXdzDvNS/QY5jggKlYXEH2+vz1BA==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
typescript: '>=3.3.1'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
||||
|
||||
@@ -7999,8 +7986,6 @@ snapshots:
|
||||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@faker-js/faker@10.3.0': {}
|
||||
|
||||
'@faker-js/faker@9.9.0': {}
|
||||
|
||||
'@feature-sliced/filesystem@3.0.1':
|
||||
@@ -8293,10 +8278,6 @@ snapshots:
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/eslint-plugin-next@15.4.0':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
optional: true
|
||||
|
||||
@@ -10812,7 +10793,7 @@ snapshots:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@1.21.7))
|
||||
@@ -10843,26 +10824,6 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.4.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.0
|
||||
'@rushstack/eslint-patch': 1.16.1
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@@ -10882,7 +10843,7 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -10923,7 +10884,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
||||
Reference in New Issue
Block a user