share job
This commit is contained in:
26
.env
Normal file
26
.env
Normal file
@@ -0,0 +1,26 @@
|
||||
# ─────────── DB (MSSQL) ───────────
|
||||
DB_URL=jdbc:sqlserver://121.156.116.136:52785;databaseName=logins_test;encrypt=false;trustServerCertificate=true
|
||||
DB_USERNAME=logins
|
||||
DB_PASSWORD=ghkfkdahrfh40-8
|
||||
|
||||
# ─────────── JWT ───────────
|
||||
# 256비트 이상 랜덤 시크릿 (운영 시 반드시 교체)
|
||||
JWT_SECRET=change-me-to-a-random-256bit-or-longer-secret-key-for-production
|
||||
|
||||
# ─────────── Redis ───────────
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# ─────────── RabbitMQ ───────────
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
|
||||
# ─────────── File Upload ───────────
|
||||
FILE_UPLOAD_PATH=/data/uploads
|
||||
|
||||
# ─────────── Spring Profile ───────────
|
||||
# mssql | mariadb
|
||||
SPRING_PROFILES_ACTIVE=mssql
|
||||
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Oracle DB
|
||||
DB_URL=jdbc:oracle:thin:@host:1521:SID
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-secret-key-must-be-at-least-256-bits-long
|
||||
|
||||
# File Storage
|
||||
FILE_UPLOAD_PATH=/data/uploads
|
||||
96
Jenkinsfile
vendored
Normal file
96
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
DOCKER_REGISTRY = "${env.DOCKER_REGISTRY ?: 'localhost:5000'}"
|
||||
IMAGE_BACKEND = "${DOCKER_REGISTRY}/gw-backend"
|
||||
IMAGE_FRONTEND = "${DOCKER_REGISTRY}/gw-frontend"
|
||||
GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
|
||||
IMAGE_TAG = "${env.BUILD_NUMBER}-${GIT_COMMIT_SHORT}"
|
||||
}
|
||||
|
||||
options {
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
timeout(time: 30, unit: 'MINUTES')
|
||||
}
|
||||
|
||||
stages {
|
||||
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend Build') {
|
||||
steps {
|
||||
dir('NEW/backend') {
|
||||
sh './gradlew clean build -x test --no-daemon'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Frontend Build') {
|
||||
steps {
|
||||
dir('NEW/frontend') {
|
||||
sh 'pnpm install --frozen-lockfile'
|
||||
sh 'pnpm build'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Docker Build & Push') {
|
||||
parallel {
|
||||
stage('Backend Image') {
|
||||
steps {
|
||||
dir('NEW/backend') {
|
||||
sh """
|
||||
docker build -t ${IMAGE_BACKEND}:${IMAGE_TAG} -t ${IMAGE_BACKEND}:latest .
|
||||
docker push ${IMAGE_BACKEND}:${IMAGE_TAG}
|
||||
docker push ${IMAGE_BACKEND}:latest
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Frontend Image') {
|
||||
steps {
|
||||
dir('NEW/frontend') {
|
||||
sh """
|
||||
docker build -t ${IMAGE_FRONTEND}:${IMAGE_TAG} -t ${IMAGE_FRONTEND}:latest .
|
||||
docker push ${IMAGE_FRONTEND}:${IMAGE_TAG}
|
||||
docker push ${IMAGE_FRONTEND}:latest
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy') {
|
||||
when {
|
||||
branch 'main'
|
||||
}
|
||||
steps {
|
||||
dir('NEW') {
|
||||
sh """
|
||||
docker-compose pull
|
||||
docker-compose up -d --no-build
|
||||
docker-compose ps
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Build ${IMAGE_TAG} deployed successfully."
|
||||
}
|
||||
failure {
|
||||
echo "Build failed. Check logs."
|
||||
}
|
||||
always {
|
||||
sh 'docker system prune -f --filter "until=24h" || true'
|
||||
}
|
||||
}
|
||||
}
|
||||
339
MIGRATION_GUIDE.md
Normal file
339
MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 그룹웨어 마이그레이션 가이드
|
||||
|
||||
## 현행 시스템 → Spring Boot + Next.js 전환
|
||||
|
||||
---
|
||||
|
||||
## 현행 시스템 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 프레임워크 | Grails 2.5.0 |
|
||||
| 언어 | Groovy / Java |
|
||||
| ORM | MyBatis (SQL XML 방식) |
|
||||
| DB | Oracle (JNDI: `java:comp/env/loginsDS`) |
|
||||
| 빌드 | Maven |
|
||||
| 뷰 | GSP + Daum Editor |
|
||||
| 버전관리 | SVN |
|
||||
|
||||
### 현행 주요 모듈
|
||||
|
||||
| 모듈 | 설명 |
|
||||
|------|------|
|
||||
| board | 게시판 |
|
||||
| tam | 결재 관리 |
|
||||
| wplan | 근무 계획 |
|
||||
| wtime | 근무 시간 관리 |
|
||||
| envset | 환경 설정 / 사용자 관리 |
|
||||
| fedex | 배송 연동 |
|
||||
| common | 공통 (인증, 코드, 첨부파일, 보안) |
|
||||
|
||||
---
|
||||
|
||||
## 신규 기술 스택 (회사 표준)
|
||||
|
||||
| 계층 | 기술 | 버전 |
|
||||
|------|------|------|
|
||||
| OS | Ubuntu | 24.04 LTS |
|
||||
| Web/Proxy | Nginx | 1.26.x Stable |
|
||||
| Frontend | Next.js | 15.x (React 19 기반) |
|
||||
| Runtime | Node.js | 22 LTS |
|
||||
| 패키지 관리 | pnpm | 최신 |
|
||||
| API Gateway | Spring Cloud Gateway | 4.x (Spring Boot 3.x 기반) |
|
||||
| Backend | Spring Boot | 3.4.x |
|
||||
| JDK | OpenJDK | 21 LTS |
|
||||
| 빌드 도구 | Gradle | 8.x |
|
||||
| DB (신규 표준) | MariaDB | 11.4 LTS |
|
||||
| DB (현재 유지) | Oracle | 기존 그대로 유지 |
|
||||
| Cache | Redis | 7.x |
|
||||
| MQ | RabbitMQ | 3.13.x |
|
||||
| 컨테이너 | Docker / Docker Compose | 최신 Stable |
|
||||
| 형상관리 | GitHub | - |
|
||||
| CI/CD | Jenkins | LTS |
|
||||
|
||||
---
|
||||
|
||||
## 신규 아키텍처
|
||||
|
||||
```
|
||||
[Nginx]
|
||||
│
|
||||
[Next.js 15.x] ──→ [Spring Cloud Gateway 4.x] ──→ [Spring Boot 3.4.x]
|
||||
│
|
||||
[Redis Cache]
|
||||
│
|
||||
[Oracle DB] ← 현재
|
||||
[MariaDB] ← 추후 전환
|
||||
```
|
||||
|
||||
### 폴더 구조
|
||||
|
||||
```
|
||||
NEW/
|
||||
backend/ ← Spring Boot 3.4.x 프로젝트 (Gradle)
|
||||
frontend/ ← Next.js 15.x 프로젝트 (pnpm)
|
||||
MIGRATION_GUIDE.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DB 전환 전략
|
||||
|
||||
### 단계별 계획
|
||||
|
||||
```
|
||||
1단계 (현재): Oracle 유지하며 신규 시스템 개발
|
||||
2단계 (추후): 신규 시스템 안정화 및 검증 완료 후 MariaDB 마이그레이션
|
||||
```
|
||||
|
||||
### SQL 이중화 전략 (MyBatis databaseId 활용)
|
||||
|
||||
기존 Oracle SQL XML을 그대로 사용하면서, MariaDB 문법 버전을 병행 작성.
|
||||
전환 시 `application.yml`의 databaseId 설정만 변경하면 즉시 교체 가능.
|
||||
|
||||
```xml
|
||||
<!-- Oracle 전용 쿼리 -->
|
||||
<select id="selectList" databaseId="oracle">
|
||||
SELECT * FROM (
|
||||
SELECT ROWNUM AS rn, t.* FROM board t WHERE ROWNUM <= #{endRow}
|
||||
) WHERE rn > #{startRow}
|
||||
</select>
|
||||
|
||||
<!-- MariaDB 전용 쿼리 (추후 전환용) -->
|
||||
<select id="selectList" databaseId="mariadb">
|
||||
SELECT * FROM board
|
||||
LIMIT #{size} OFFSET #{offset}
|
||||
</select>
|
||||
```
|
||||
|
||||
### Oracle → MariaDB 주요 변환 포인트
|
||||
|
||||
| 항목 | Oracle | MariaDB |
|
||||
|------|--------|---------|
|
||||
| 페이징 | ROWNUM | LIMIT / OFFSET |
|
||||
| 시퀀스 | sequence.NEXTVAL | AUTO_INCREMENT |
|
||||
| 날짜 함수 | SYSDATE, TO_DATE | NOW(), STR_TO_DATE |
|
||||
| 문자열 연결 | `||` | CONCAT() 또는 `||` |
|
||||
| NVL | NVL() | IFNULL() / COALESCE() |
|
||||
| DECODE | DECODE() | CASE WHEN |
|
||||
| 가상 테이블 | FROM DUAL | FROM DUAL (지원) 또는 생략 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1. 분석 및 설계
|
||||
|
||||
### 1-1. 현행 파악 체크리스트
|
||||
|
||||
- [ ] 각 컨트롤러의 URL 매핑 목록화 (`UrlMappings.groovy` + 각 컨트롤러)
|
||||
- [ ] 세션/인증 방식 파악 (`SecurityFilter`, `AuthService`)
|
||||
- [ ] 공통 유틸 파악 (`CommonUtil`, `ReqUtil`, `FormatUtil` 등)
|
||||
- [ ] SQL XML의 쿼리별 입출력 파라미터 파악
|
||||
- [ ] 파일 첨부 저장 방식 파악 (`AttachService`)
|
||||
- [ ] 메뉴/권한 구조 파악 (`MenuService`, `SecurityService`)
|
||||
|
||||
### 1-2. 인증 구조 전환 설계
|
||||
|
||||
```
|
||||
기존: 세션 기반 (SecurityFilter.groovy)
|
||||
신규: JWT 기반
|
||||
- POST /api/auth/login → JWT 발급
|
||||
- 모든 요청 헤더: Authorization: Bearer {token}
|
||||
- Spring Security Filter Chain 구성
|
||||
- Redis를 활용한 토큰 블랙리스트/Refresh Token 관리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2. Spring Boot 백엔드 구성
|
||||
|
||||
### 2-1. 주요 의존성 (build.gradle)
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
// Web
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
// Security & JWT
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.12.x'
|
||||
|
||||
// MyBatis
|
||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.x'
|
||||
|
||||
// Oracle JDBC
|
||||
implementation 'com.oracle.database.jdbc:ojdbc11'
|
||||
|
||||
// MariaDB JDBC (추후 전환용)
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// Validation
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
|
||||
// Lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// Excel (Apache POI)
|
||||
implementation 'org.apache.poi:poi-ooxml:5.x'
|
||||
}
|
||||
```
|
||||
|
||||
### 2-2. MyBatis 마이그레이션 (기존 SQL XML 재사용)
|
||||
|
||||
기존 `src/java/sql/` 의 XML 파일을 Spring Boot 프로젝트로 복사 후 경미한 수정만 필요.
|
||||
|
||||
| 작업 | 내용 |
|
||||
|------|------|
|
||||
| Mapper XML 이동 | `src/main/resources/mapper/` 로 복사 |
|
||||
| Mapper Interface 생성 | XML의 각 쿼리 ID에 대응하는 Java Interface 작성 |
|
||||
| VO/DTO 클래스 | Groovy VO → Java POJO (Lombok 활용) |
|
||||
| SqlSessionFactory | MyBatis Spring Boot Starter 자동 설정 |
|
||||
| databaseId 설정 | Oracle/MariaDB 구분을 위한 DatabaseIdProvider 등록 |
|
||||
|
||||
### 2-3. 모듈별 변환 순서
|
||||
|
||||
```
|
||||
1. common → 인증(JWT + Redis), 코드, 공통 유틸
|
||||
2. envset → 사용자/권한/메뉴 관리
|
||||
3. board → 게시판 (전형적인 CRUD로 패턴 확립)
|
||||
4. tam → 결재 워크플로우
|
||||
5. wplan → 근무 계획
|
||||
6. wtime → 근무 시간
|
||||
7. fedex → 외부 연동
|
||||
```
|
||||
|
||||
### 2-4. API 응답 공통 형식
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... },
|
||||
"message": "",
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3. Next.js 프론트엔드 구성
|
||||
|
||||
### 3-1. 주요 의존성
|
||||
|
||||
```
|
||||
- Next.js 15.x (App Router, React 19)
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- shadcn/ui (UI 컴포넌트)
|
||||
- TanStack Query v5 (서버 상태 관리)
|
||||
- Zustand (클라이언트 상태 관리)
|
||||
- react-hook-form + zod (폼 검증)
|
||||
- axios (API 호출)
|
||||
- Toast UI Editor 또는 Tiptap (WYSIWYG - Daum Editor 대체)
|
||||
```
|
||||
|
||||
### 3-2. 페이지 구조
|
||||
|
||||
```
|
||||
/app
|
||||
/login
|
||||
/(main)
|
||||
/dashboard
|
||||
/board/[boardType]
|
||||
/tam ← 결재
|
||||
/wplan ← 근무계획
|
||||
/wtime ← 근무시간
|
||||
/envset ← 환경설정
|
||||
/users
|
||||
/codes
|
||||
/menus
|
||||
```
|
||||
|
||||
### 3-3. GSP → Next.js 화면 변환
|
||||
|
||||
```
|
||||
Daum Editor → Toast UI Editor 또는 Tiptap
|
||||
GSP 페이징 태그 → 커스텀 Pagination 컴포넌트
|
||||
GSP 공통 레이아웃 → app/layout.tsx
|
||||
세션 체크 → JWT 기반 Next.js Middleware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4. 인프라 구성 (Docker)
|
||||
|
||||
### docker-compose.yml 구성 예시
|
||||
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.26
|
||||
frontend:
|
||||
build: ./frontend # Next.js
|
||||
gateway:
|
||||
build: ./gateway # Spring Cloud Gateway
|
||||
backend:
|
||||
build: ./backend # Spring Boot
|
||||
redis:
|
||||
image: redis:7
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.13-management
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5. 통합 및 검증
|
||||
|
||||
### 5-1. 모듈별 검증 체크리스트
|
||||
|
||||
- [ ] 기존 SQL 결과와 신규 API 응답값 동일 여부 확인
|
||||
- [ ] 페이징, 검색 조건 동작 확인
|
||||
- [ ] 파일 첨부/다운로드 동작 확인
|
||||
- [ ] 결재 워크플로우 흐름 검증
|
||||
- [ ] 권한/메뉴 제어 동작 확인
|
||||
- [ ] Excel 다운로드 동작 확인
|
||||
|
||||
### 5-2. 병행 운영 전략
|
||||
|
||||
```
|
||||
1단계: 기존 Grails 운영 유지
|
||||
2단계: 신규 시스템 완성 모듈부터 부분 교체
|
||||
3단계: 전체 전환 후 기존 시스템 종료
|
||||
4단계: DB를 Oracle → MariaDB 마이그레이션
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업량 예상
|
||||
|
||||
| 영역 | 난이도 | 이유 |
|
||||
|------|--------|------|
|
||||
| MyBatis SQL 재사용 | 낮음 | XML 거의 그대로 사용 가능 |
|
||||
| SQL 이중화 (Oracle+MariaDB) | 중간 | databaseId로 병행 관리 |
|
||||
| Spring Boot API 변환 | 중간 | 서비스 로직 Groovy → Java 변환 |
|
||||
| JWT + Redis 인증 | 중간 | 세션 → Stateless 구조 변경 |
|
||||
| 결재(tam) 로직 | 높음 | 워크플로우 복잡도 |
|
||||
| Next.js 화면 재작성 | 높음 | 전체 UI 새로 작성 필요 |
|
||||
| 파일 첨부 처리 | 중간 | 저장 경로/방식 재설계 필요 |
|
||||
| Docker 인프라 구성 | 중간 | 서비스별 컨테이너화 |
|
||||
|
||||
---
|
||||
|
||||
## 진행 현황
|
||||
|
||||
- [x] NEW 폴더 및 하위 구조 생성
|
||||
- [x] 마이그레이션 가이드 문서 작성 (기술 스택 확정)
|
||||
- [ ] Spring Boot 백엔드 프로젝트 초기 세팅
|
||||
- [ ] Next.js 프론트엔드 프로젝트 초기 세팅
|
||||
- [ ] Phase 1: 현행 분석
|
||||
- [ ] Phase 2: 백엔드 구축
|
||||
- [ ] Phase 3: 프론트엔드 구축
|
||||
- [ ] Phase 4: 인프라(Docker) 구성
|
||||
- [ ] Phase 5: 통합 및 검증
|
||||
- [ ] DB 마이그레이션 (Oracle → MariaDB)
|
||||
334
TASK_LIST.md
Normal file
334
TASK_LIST.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# 그룹웨어 마이그레이션 작업 목록
|
||||
|
||||
> **범례**: ✅ 완료 | 🔄 진행중 | ⬜ 미착수
|
||||
> **원칙**: 각 모듈은 백엔드(SQL → Mapper → Service → Controller) → 프론트엔드(API → Types → Page) 순서로 진행
|
||||
|
||||
---
|
||||
|
||||
## 0. 프로젝트 기반 구성
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 0-1 | NEW 폴더 구조 생성 | ✅ | backend/, frontend/ |
|
||||
| 0-2 | MIGRATION_GUIDE.md 작성 | ✅ | DB 전략, 기술 스택 확정 |
|
||||
| 0-3 | Spring Boot 프로젝트 초기 세팅 | ✅ | Gradle, JDK 21, Spring Boot 3.4.x |
|
||||
| 0-4 | Next.js 프로젝트 초기 세팅 | ✅ | pnpm, TypeScript, Tailwind |
|
||||
| 0-5 | DB 설정 (Oracle → MS SQL Server 수정) | ✅ | mssql-jdbc, application-mssql.yml |
|
||||
| 0-6 | docker-compose.yml 초안 작성 | ✅ | Nginx, Backend, Frontend, Redis, RabbitMQ |
|
||||
| 0-7 | Nginx 설정 파일 작성 | ✅ | nginx/nginx.conf (API 프록시 + 파일 서빙) |
|
||||
| 0-8 | Dockerfile 최종 검증 (backend/frontend) | ✅ | backend/Dockerfile, frontend/Dockerfile 존재 |
|
||||
| 0-9 | GitHub 레포지토리 연결 | ⬜ | .gitignore, 초기 커밋 |
|
||||
| 0-10 | Jenkins CI/CD 파이프라인 작성 | ✅ | Jenkinsfile (parallel build + deploy) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 공통 기반 (Common)
|
||||
|
||||
### 1-1. 백엔드 공통
|
||||
| # | 작업 | 상태 | 대상 파일 (원본) |
|
||||
|---|------|------|------|
|
||||
| 1-1-1 | JWT 유틸 (corpNo 포함) | ✅ | JwtUtil.java |
|
||||
| 1-1-2 | JWT 인증 필터 | ✅ | JwtAuthenticationFilter.java |
|
||||
| 1-1-3 | Spring Security 설정 | ✅ | SecurityConfig.java |
|
||||
| 1-1-4 | Redis 설정 | ✅ | RedisConfig.java |
|
||||
| 1-1-5 | MyBatis 설정 (DatabaseId 포함) | ✅ | MyBatisConfig.java |
|
||||
| 1-1-6 | 공통 응답 포맷 (ApiResponse) | ✅ | ApiResponse.java |
|
||||
| 1-1-7 | 공통 예외 처리 | ✅ | GlobalExceptionHandler.java, BizException.java |
|
||||
| 1-1-8 | CurrentUser 유틸 + SecurityUtil | ✅ | CurrentUser.java, SecurityUtil.java |
|
||||
| 1-1-9 | 시퀀스 서비스 | ✅ | SequenceService.java (SX_CO0060) |
|
||||
| 1-1-10 | 파일 첨부 서비스 | ✅ | AttachService.java, AttachController.java |
|
||||
| 1-1-11 | 코드 조회 서비스 (공통코드 캐시) | ✅ | CodeService.java (@Cacheable Redis, TTL 6h) |
|
||||
| 1-1-12 | 이메일 서비스 | ⬜ | EmailService.groovy → EmailService.java |
|
||||
| 1-1-13 | 공통 SQL XML 이식 | ✅ | common_sql.xml (시퀀스) |
|
||||
| 1-1-14 | attach SQL XML 이식 | ✅ | attach_sql.xml (MSSQL + MariaDB) |
|
||||
| 1-1-15 | code SQL XML 이식 | ✅ | code_sql.xml (getCodeList, searchUser, getWorkCdList) |
|
||||
|
||||
### 1-2. 프론트엔드 공통
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 1-2-1 | Axios 클라이언트 + 401 자동 갱신 | ✅ | lib/api/client.ts |
|
||||
| 1-2-2 | Zustand 인증 스토어 | ✅ | lib/store/authStore.ts |
|
||||
| 1-2-3 | TanStack Query Provider | ✅ | components/common/Providers.tsx |
|
||||
| 1-2-4 | JWT 미들웨어 (라우트 보호) | ✅ | middleware.ts |
|
||||
| 1-2-5 | 공통 레이아웃 (Sidebar + Header) | ✅ | components/layout/ |
|
||||
| 1-2-6 | 공통 타입 정의 | ✅ | types/common.ts, types/auth.ts |
|
||||
| 1-2-7 | 공통 UI 컴포넌트 (Modal, Pagination, Table) | ✅ | Modal.tsx, Pagination.tsx 완성 |
|
||||
| 1-2-8 | 파일 첨부 컴포넌트 | ✅ | components/common/FileUpload.tsx |
|
||||
| 1-2-9 | Toast 알림 컴포넌트 | ✅ | Toast.tsx (ToastProvider + useToast hook) |
|
||||
| 1-2-10 | 날짜 선택 컴포넌트 | ⬜ | components/common/DatePicker.tsx |
|
||||
|
||||
---
|
||||
|
||||
## 2. 인증 (Auth / Index)
|
||||
|
||||
> 원본: `IndexController`, `SecurityFilter`, `AuthService`, `SecurityService`, `UserService`
|
||||
|
||||
### 2-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 2-1-1 | user_sql.xml 이식 | ✅ | getUserInfo, getUserInfoWithPwd |
|
||||
| 2-1-2 | security_sql.xml 이식 | ✅ | getUserRoleList |
|
||||
| 2-1-3 | menu_sql.xml 이식 | ✅ | getMenuList, getControllerRoleList |
|
||||
| 2-1-4 | UserMapper / SecurityMapper / MenuMapper | ✅ | |
|
||||
| 2-1-5 | PasswordUtil (SHA-256 + Base64) | ✅ | 기존 DB 비밀번호 호환 |
|
||||
| 2-1-6 | AuthService (로그인/로그아웃/토큰재발급) | ✅ | Redis RefreshToken |
|
||||
| 2-1-7 | CustomUserDetailsService | ✅ | |
|
||||
| 2-1-8 | AuthController (login/logout/refresh/me) | ✅ | |
|
||||
|
||||
### 2-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 2-2-1 | authApi (login/logout/refresh) | ✅ | lib/api/auth.ts |
|
||||
| 2-2-2 | 로그인 페이지 + LoginForm | ✅ | app/login/page.tsx |
|
||||
| 2-2-3 | 권한(MENU_AUTH_CD) 선택 드롭다운 | ✅ | 기존 시스템 동일 |
|
||||
| 2-2-4 | 로그아웃 처리 (Header) | ✅ | |
|
||||
| 2-2-5 | 아이디 저장 기능 | ✅ | localStorage gw_save_id/gw_saved_login_id |
|
||||
|
||||
---
|
||||
|
||||
## 3. 환경설정 (Envset)
|
||||
|
||||
### 3-1. 직원정보 (Envset0010 / SX_GW0010)
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 3-1-1 | envset0010_sql.xml 이식 | ✅ | 페이징 포함 |
|
||||
| 3-1-2 | UserManageMapper | ✅ | |
|
||||
| 3-1-3 | UserManageService | ✅ | 시퀀스 ID 생성, PW 암호화 |
|
||||
| 3-1-4 | UserManageController | ✅ | CRUD REST API |
|
||||
| 3-1-5 | 직원 목록 페이지 | ✅ | 마스터-디테일 구조 |
|
||||
| 3-1-6 | 직원 등록/수정 폼 | ✅ | |
|
||||
| 3-1-7 | 직원 사진 업로드 | ✅ | FileUpload 컴포넌트 연동 (users/page.tsx) |
|
||||
| 3-1-8 | 근무코드 관리 (Envset0040, SX_CO0070) | ✅ | envset0040_sql.xml + WorkCdMapper/Service/Controller + workcd/page.tsx |
|
||||
|
||||
### 3-2. 공통코드 관리 (Envset0020/0030 / SX_CO0030, SX_CO0040)
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 3-2-1 | envset0020_sql.xml 이식 | ✅ | |
|
||||
| 3-2-2 | CodeManageMapper / Service / Controller | ✅ | 배치 저장 (I/U/D) |
|
||||
| 3-2-3 | 코드 관리 페이지 | ✅ | 코드인덱스 + 코드 |
|
||||
|
||||
### 3-3. 메뉴/권한 관리 (Envset0050 / SX_CO0080, SX_CO0090, SX_GW0130)
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 3-3-1 | envset0050_sql.xml 이식 | ✅ | MERGE INTO 포함 |
|
||||
| 3-3-2 | MenuManageMapper / Service / Controller | ✅ | |
|
||||
| 3-3-3 | 메뉴 관리 페이지 | ✅ | |
|
||||
| 3-3-4 | 권한별 메뉴 설정 페이지 | ✅ | 체크박스 토글 |
|
||||
| 3-3-5 | 사용자 권한 관리 페이지 | ✅ | 사용자 autocomplete |
|
||||
|
||||
---
|
||||
|
||||
## 4. 게시판 (Board)
|
||||
|
||||
> 원본: `Board0010Controller`, `Board_0001~0008Controller`, `board0010_sql.xml`
|
||||
> 8개 게시판 유형을 하나의 Controller로 통합 (boardType 파라미터로 구분)
|
||||
|
||||
### 4-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 4-1-1 | board0010_sql.xml 분석 및 이식 | ✅ | MSSQL + MariaDB 이중 작성 |
|
||||
| 4-1-2 | BoardMapper 인터페이스 | ✅ | |
|
||||
| 4-1-3 | BoardService | ✅ | 목록/상세/등록/수정/삭제/조회수/댓글 |
|
||||
| 4-1-4 | BoardController | ✅ | `/api/board/{boardType}` REST |
|
||||
| 4-1-5 | 파일 첨부 연동 | ✅ | AttachService 연동 완료 |
|
||||
|
||||
### 4-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 4-2-1 | boardApi 정의 | ✅ | lib/api/board.ts |
|
||||
| 4-2-2 | 게시판 목록 페이지 | ✅ | app/(main)/board/[boardType]/page.tsx |
|
||||
| 4-2-3 | 게시물 상세 페이지 | ✅ | 댓글 포함 |
|
||||
| 4-2-4 | 게시물 등록/수정 페이지 | ✅ | PostForm 컴포넌트 |
|
||||
| 4-2-5 | 파일 첨부 UI | ✅ | FileUpload 컴포넌트 연동 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 결재 (TAM)
|
||||
|
||||
> 원본: `Tam0010~0040Controller`, `tam0010~0040_sql.xml`, `ApvdocService`, `apvdoc_sql.xml`
|
||||
> 가장 복잡한 모듈 - 워크플로우 로직 포함
|
||||
|
||||
### 5-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 5-1-1 | tam_sql.xml 분석 및 이식 (0010~0040 통합) | ✅ | MSSQL + MariaDB 이중 작성 |
|
||||
| 5-1-2 | apvdoc_sql.xml 분석 및 이식 | ✅ | 결재 문서 공통, DECLARE @var → subquery |
|
||||
| 5-1-3 | TamMapper / ApvdocMapper | ✅ | |
|
||||
| 5-1-4 | ApvdocService (워크플로우 로직) | ✅ | 결재선, 승인/반려, 상태 변경 |
|
||||
| 5-1-5 | TamService | ✅ | TAM0010~0040 서비스 |
|
||||
| 5-1-6 | TamController | ✅ | `/api/tam/` REST |
|
||||
|
||||
### 5-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 5-2-1 | tamApi 정의 | ✅ | lib/api/tam.ts |
|
||||
| 5-2-2 | 연차 관리 페이지 (TAM0010) | ✅ | 인라인 편집 |
|
||||
| 5-2-3 | 결재 신청 목록 페이지 (TAM0020) | ✅ | 상태/종류 필터 |
|
||||
| 5-2-4 | 결재 신청 등록/상세 페이지 | ✅ | ApvreqForm + ApproverSection 컴포넌트 |
|
||||
| 5-2-5 | 결재 처리 목록/상세 페이지 (TAM0030) | ✅ | 승인/반려 처리 |
|
||||
| 5-2-6 | 근태 현황 페이지 (TAM0040) | ✅ | 종류별 집계 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 근무계획 (Wplan)
|
||||
|
||||
> 원본: `Wplan0010~0030Controller`, `wplan0010~0030_sql.xml`
|
||||
|
||||
### 6-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 6-1-1 | wplan_sql.xml 분석 및 이식 (0010~0030 통합) | ✅ | MSSQL + MariaDB 이중 작성 |
|
||||
| 6-1-2 | WplanMapper / Service / Controller | ✅ | `/api/wplan/` |
|
||||
|
||||
### 6-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 6-2-1 | wplanApi 정의 | ✅ | lib/api/wplan.ts |
|
||||
| 6-2-2 | 근무계획 관리 페이지 (Wplan0010) | ✅ | 월별 피벗 테이블 + 인라인 편집 |
|
||||
| 6-2-3 | 나의근무계획 페이지 (Wplan0020) | ✅ | 월 달력 뷰 |
|
||||
| 6-2-4 | 전체근무계획 페이지 (Wplan0030) | ✅ | 근무코드 필터 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 근무시간 (Wtime)
|
||||
|
||||
> 원본: `Wtime0010Controller`, `Wtime0030Controller`, `wtime0010_sql.xml`, `wtime0030_sql.xml`, `work_sql.xml`
|
||||
|
||||
### 7-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 7-1-1 | wtime_sql.xml 분석 및 이식 (0010+0030 통합) | ✅ | MSSQL + MariaDB 이중 작성 |
|
||||
| 7-1-2 | WtimeMapper / Service / Controller | ✅ | `/api/wtime/` |
|
||||
| 7-1-3 | WorkService (근무시간 재계산 로직) | ⬜ | 추후 필요 시 추가 |
|
||||
|
||||
### 7-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 7-2-1 | wtimeApi 정의 | ✅ | lib/api/wtime.ts |
|
||||
| 7-2-2 | 개인별 근무시간 페이지 (Wtime0010) | ✅ | 날짜 범위 검색 |
|
||||
| 7-2-3 | 월별 근무시간 집계 페이지 (Wtime0030) | ✅ | 개인별 합산 |
|
||||
|
||||
---
|
||||
|
||||
## 8. FedEx 배송 (Fedex)
|
||||
|
||||
> 원본: `Fedex0010Controller`, `fedex0010_sql.xml`
|
||||
|
||||
### 8-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 8-1-1 | fedex0010_sql.xml 분석 및 이식 | ✅ | MSSQL + MariaDB, errorMng 테이블 |
|
||||
| 8-1-2 | FedexMapper / Service / Controller | ✅ | `/api/fedex/` |
|
||||
|
||||
### 8-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 8-2-1 | fedexApi 정의 | ✅ | lib/api/fedex.ts |
|
||||
| 8-2-2 | FedEx 수입정정 목록 페이지 | ✅ | fedex/0010/page.tsx |
|
||||
| 8-2-3 | FedEx 수입정정 등록/상세 페이지 | ✅ | write + [sq] 페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 메인/대시보드 (Main)
|
||||
|
||||
> 원본: `Main0010Controller`, `Main0020Controller`, `main0010_sql.xml`, `main0020_sql.xml`
|
||||
|
||||
### 9-1. 백엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 9-1-1 | main0010_sql.xml 분석 및 이식 | ✅ | main_sql.xml (직접 SQL, stored proc 불사용) |
|
||||
| 9-1-2 | main0020_sql.xml 분석 및 이식 | ✅ | main_sql.xml 통합 |
|
||||
| 9-1-3 | MainMapper / Service / Controller | ✅ | `/api/main/dashboard` |
|
||||
|
||||
### 9-2. 프론트엔드
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 9-2-1 | mainApi 정의 | ✅ | lib/api/main.ts |
|
||||
| 9-2-2 | 대시보드 페이지 | ✅ | 결재 대기 배너, 공지/게시판/근무 위젯 |
|
||||
| 9-2-3 | 메인 레이아웃 완성 | ✅ | Sidebar 동적 메뉴 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 공통 기능 (파일첨부 / 이메일)
|
||||
|
||||
### 10-1. 파일 첨부 (AttachService)
|
||||
> 원본: `AttachController`, `AttachService`, `attach_sql.xml`
|
||||
> 모든 모듈(게시판, 결재, 직원사진)에서 사용
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 10-1-1 | attach_sql.xml 분석 및 이식 | ✅ | attach_sql.xml (MSSQL + MariaDB) |
|
||||
| 10-1-2 | AttachMapper | ✅ | AttachMapper.java |
|
||||
| 10-1-3 | AttachService (업로드/다운로드/삭제) | ✅ | 로컬 저장소, 확장자 검증 |
|
||||
| 10-1-4 | AttachController | ✅ | `/api/attach/` Multipart + download |
|
||||
| 10-1-5 | FileUpload 컴포넌트 | ✅ | FileUpload.tsx (드래그앤드롭) |
|
||||
| 10-1-6 | 파일 다운로드 처리 | ✅ | Content-Disposition URLEncoder |
|
||||
|
||||
### 10-2. 이메일 서비스
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 10-2-1 | EmailService 이식 | ⬜ | Spring Mail (SMTP) |
|
||||
| 10-2-2 | 결재 알림 이메일 연동 | ⬜ | TAM 모듈 완성 후 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 인프라 및 배포
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 11-1 | Nginx 설정 (nginx.conf) | ✅ | API 프록시 + 파일 서빙 |
|
||||
| 11-2 | docker-compose.yml 최종 완성 | ✅ | env_file, healthcheck, volumes |
|
||||
| 11-3 | .env 파일 구성 | ✅ | DB, JWT, Redis, RabbitMQ |
|
||||
| 11-4 | Jenkins Jenkinsfile 작성 | ✅ | parallel build + deploy |
|
||||
| 11-5 | Spring Boot actuator 헬스체크 | ✅ | /actuator/health (SecurityConfig 허용) |
|
||||
| 11-6 | 로그 설정 (logback-spring.xml) | ✅ | 프로파일별 콘솔/파일 분리 |
|
||||
|
||||
---
|
||||
|
||||
## 12. DB 마이그레이션 준비 (SQL Server → MariaDB)
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 12-1 | 전체 SQL XML MariaDB 버전 작성 | ✅ | 전체 17개 XML 파일 완료 |
|
||||
| 12-2 | GETDATE() → NOW() 변환 | ✅ | 모든 DML 문 MariaDB 버전에 반영 |
|
||||
| 12-3 | ISNULL() → IFNULL() 변환 | ✅ | MariaDB databaseId 버전에 반영 |
|
||||
| 12-4 | MERGE INTO → INSERT ON DUPLICATE KEY | ✅ | common_sql, envset0050_sql 반영 |
|
||||
| 12-5 | SELECT TOP N → LIMIT N 변환 | ✅ | MariaDB 버전에 LIMIT 적용 |
|
||||
| 12-6 | OFFSET...FETCH NEXT → LIMIT...OFFSET 변환 | ✅ | 페이징 쿼리 전체 반영 |
|
||||
| 12-7 | 데이터 마이그레이션 스크립트 작성 | ⬜ | SQL Server → MariaDB |
|
||||
| 12-8 | application-mariadb.yml 완성 | ⬜ | |
|
||||
| 12-9 | MariaDB 환경에서 전체 기능 검증 | ⬜ | |
|
||||
|
||||
---
|
||||
|
||||
## 13. 검증 및 완료
|
||||
|
||||
| # | 작업 | 상태 | 비고 |
|
||||
|---|------|------|------|
|
||||
| 13-1 | API 전체 목록 문서화 (Swagger) | ⬜ | springdoc-openapi |
|
||||
| 13-2 | 기존 시스템 대비 기능 동등성 검증 | ⬜ | 모듈별 체크리스트 |
|
||||
| 13-3 | 성능 테스트 (페이징, 대용량 조회) | ⬜ | |
|
||||
| 13-4 | 보안 점검 (SQL Injection, XSS, CSRF) | ⬜ | |
|
||||
| 13-5 | 병행 운영 계획 수립 | ⬜ | 기존 Grails 유지 기간 결정 |
|
||||
| 13-6 | 운영 전환 (Cut-over) | ⬜ | |
|
||||
|
||||
---
|
||||
|
||||
## 진행 현황 요약
|
||||
|
||||
```
|
||||
전체 작업 수: 약 130개
|
||||
완료: 약 40개 (31%)
|
||||
진행중: 0개
|
||||
미착수: 약 90개 (69%)
|
||||
```
|
||||
|
||||
### 권장 다음 작업 순서
|
||||
1. **1-1-10** 파일 첨부 서비스 → 게시판, 결재, 직원사진 모두 필요
|
||||
2. **4** 게시판 → 가장 단순한 CRUD, 패턴 확립용
|
||||
3. **5** 결재 → 핵심 업무 기능 (복잡도 높음)
|
||||
4. **6, 7** 근무계획/근무시간
|
||||
5. **8** FedEx
|
||||
6. **9** 대시보드
|
||||
7. **11** 인프라
|
||||
8. **12** MariaDB 전환 준비
|
||||
10
backend/.gitignore
vendored
Normal file
10
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.gradle/
|
||||
build/
|
||||
*.class
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
.idea/
|
||||
*.iml
|
||||
out/
|
||||
.env
|
||||
application-local.yml
|
||||
9
backend/Dockerfile
Normal file
9
backend/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM openjdk:21-jdk-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN ./gradlew bootJar -x test
|
||||
|
||||
FROM openjdk:21-jdk-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/build/libs/*.jar app.jar
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
92
backend/build.gradle
Normal file
92
backend/build.gradle
Normal file
@@ -0,0 +1,92 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.4.1'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'com.company'
|
||||
version = '1.0.0-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
set('springCloudVersion', '2024.0.0')
|
||||
set('mybatisVersion', '3.0.4')
|
||||
set('jjwtVersion', '0.12.6')
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Web
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
// Actuator (헬스체크)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// Security
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
|
||||
// JWT
|
||||
implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
|
||||
|
||||
// MyBatis
|
||||
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisVersion}"
|
||||
|
||||
// MS SQL Server JDBC (현재 사용) - 9.4.1: encrypt=false 시 TLS 미사용 (TLS1.0 서버 호환)
|
||||
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc:9.4.1.jre11'
|
||||
|
||||
// MariaDB JDBC (추후 전환용)
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// RabbitMQ
|
||||
implementation 'org.springframework.boot:spring-boot-starter-amqp'
|
||||
|
||||
// Validation
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
|
||||
// Lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// Excel (Apache POI)
|
||||
implementation 'org.apache.poi:poi-ooxml:5.3.0'
|
||||
|
||||
// Test
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4'
|
||||
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
bootRun {
|
||||
args '--spring.profiles.active=mssql,local'
|
||||
jvmArgs "-Djava.security.properties=${projectDir}/tls-local.security"
|
||||
}
|
||||
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
backend/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
backend/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
backend/gradlew
vendored
Normal file
251
backend/gradlew
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
backend/gradlew.bat
vendored
Normal file
94
backend/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
1
backend/settings.gradle
Normal file
1
backend/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'gw-backend'
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.company.gw;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableCaching
|
||||
public class GwBackendApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GwBackendApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.company.gw.board.controller;
|
||||
|
||||
import com.company.gw.board.service.BoardService;
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/board")
|
||||
@RequiredArgsConstructor
|
||||
public class BoardController {
|
||||
|
||||
private final BoardService boardService;
|
||||
|
||||
/** 게시물 목록 */
|
||||
@GetMapping("/{untyBbsCd}")
|
||||
public ApiResponse<Map<String, Object>> getPostList(
|
||||
@PathVariable String untyBbsCd,
|
||||
@RequestParam(defaultValue = "") String searchText,
|
||||
@RequestParam(defaultValue = "1") int pageNo,
|
||||
@RequestParam(defaultValue = "20") int pageSize,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(
|
||||
boardService.getPostList(currentUser.getCorpNo(), untyBbsCd, searchText, pageNo, pageSize));
|
||||
}
|
||||
|
||||
/** 게시물 상세 (조회수 증가) */
|
||||
@GetMapping("/{untyBbsCd}/{untyBbsSno}")
|
||||
public ApiResponse<Map<String, Object>> getPostInfo(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(
|
||||
boardService.getPostInfoForView(currentUser.getCorpNo(), untyBbsCd, untyBbsSno));
|
||||
}
|
||||
|
||||
/** 게시물 상세 (수정용, 조회수 미증가) */
|
||||
@GetMapping("/{untyBbsCd}/{untyBbsSno}/edit")
|
||||
public ApiResponse<Map<String, Object>> getPostInfoForEdit(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(
|
||||
boardService.getPostInfoForEdit(currentUser.getCorpNo(), untyBbsCd, untyBbsSno));
|
||||
}
|
||||
|
||||
/** 게시물 등록 */
|
||||
@PostMapping("/{untyBbsCd}")
|
||||
public ApiResponse<Map<String, Object>> insertPost(
|
||||
@PathVariable String untyBbsCd,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
body.put("untyBbsCd", untyBbsCd);
|
||||
return ApiResponse.ok(boardService.insertPost(currentUser.getCorpNo(), body));
|
||||
}
|
||||
|
||||
/** 게시물 수정 */
|
||||
@PutMapping("/{untyBbsCd}/{untyBbsSno}")
|
||||
public ApiResponse<Void> updatePost(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
body.put("untyBbsCd", untyBbsCd);
|
||||
boardService.updatePost(currentUser.getCorpNo(), untyBbsSno, body);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 게시물 삭제 */
|
||||
@DeleteMapping("/{untyBbsCd}/{untyBbsSno}")
|
||||
public ApiResponse<Void> deletePost(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
boardService.deletePost(currentUser.getCorpNo(), untyBbsCd, untyBbsSno);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 댓글 목록 */
|
||||
@GetMapping("/{untyBbsCd}/{untyBbsSno}/comments")
|
||||
public ApiResponse<List<Map<String, Object>>> getCommentList(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(
|
||||
boardService.getCommentList(currentUser.getCorpNo(), untyBbsCd, untyBbsSno));
|
||||
}
|
||||
|
||||
/** 댓글 등록 */
|
||||
@PostMapping("/{untyBbsCd}/{untyBbsSno}/comments")
|
||||
public ApiResponse<Void> insertComment(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@RequestBody Map<String, String> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
boardService.insertComment(currentUser.getCorpNo(), untyBbsCd, untyBbsSno, body.get("cmmtCn"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 댓글 삭제 */
|
||||
@DeleteMapping("/{untyBbsCd}/{untyBbsSno}/comments/{cmmtSno}")
|
||||
public ApiResponse<Void> deleteComment(
|
||||
@PathVariable String untyBbsCd,
|
||||
@PathVariable Long untyBbsSno,
|
||||
@PathVariable Long cmmtSno,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
boardService.deleteComment(currentUser.getCorpNo(), untyBbsCd, untyBbsSno, cmmtSno);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.company.gw.board.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface BoardMapper {
|
||||
|
||||
List<Map<String, Object>> getPostList(Map<String, Object> param);
|
||||
|
||||
long getPostListCount(Map<String, Object> param);
|
||||
|
||||
Map<String, Object> getPostInfo(Map<String, Object> param);
|
||||
|
||||
void insertPost(Map<String, Object> param);
|
||||
|
||||
void updatePost(Map<String, Object> param);
|
||||
|
||||
void increaseInqrCnt(Map<String, Object> param);
|
||||
|
||||
void updateEtcAtchNo(Map<String, Object> param);
|
||||
|
||||
void updatePhotoAtchNo(Map<String, Object> param);
|
||||
|
||||
void updateCmmtCnt(Map<String, Object> param);
|
||||
|
||||
void deletePost(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> getCommentList(Map<String, Object> param);
|
||||
|
||||
Map<String, Object> getCommentInfo(Map<String, Object> param);
|
||||
|
||||
void insertComment(Map<String, Object> param);
|
||||
|
||||
void deleteComment(Map<String, Object> param);
|
||||
|
||||
void deleteCommentsByPost(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.company.gw.board.service;
|
||||
|
||||
import com.company.gw.board.mapper.BoardMapper;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.service.AttachService;
|
||||
import com.company.gw.common.service.SequenceService;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BoardService {
|
||||
|
||||
private final BoardMapper boardMapper;
|
||||
private final SequenceService sequenceService;
|
||||
private final AttachService attachService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 게시물 목록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> getPostList(String corpNo, String untyBbsCd,
|
||||
String searchText, int pageNo, int pageSize) {
|
||||
if (pageSize > 200) pageSize = 200;
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("untyBbsCd", untyBbsCd);
|
||||
param.put("searchText", searchText);
|
||||
param.put("pageNo", pageNo);
|
||||
param.put("pageSize", pageSize);
|
||||
|
||||
List<Map<String, Object>> list = boardMapper.getPostList(param);
|
||||
long total = boardMapper.getPostListCount(param);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
result.put("pageNo", pageNo);
|
||||
result.put("pageSize", pageSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 게시물 상세 (조회수 증가 포함)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> getPostInfoForView(String corpNo, String untyBbsCd, Long untyBbsSno) {
|
||||
Map<String, Object> param = buildPostParam(corpNo, untyBbsCd, untyBbsSno);
|
||||
|
||||
boardMapper.increaseInqrCnt(param);
|
||||
|
||||
Map<String, Object> info = boardMapper.getPostInfo(param);
|
||||
if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
|
||||
// 첨부파일 목록 조회
|
||||
String etcAtchNo = (String) info.get("ETC_ATCH_NO");
|
||||
String photoAtchNo = (String) info.get("PHOTO_ATCH_NO");
|
||||
if (etcAtchNo != null) info.put("etcFileList", attachService.getFileList(etcAtchNo));
|
||||
if (photoAtchNo != null) info.put("photoFileList", attachService.getFileList(photoAtchNo));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// 수정화면용 (조회수 증가 없음)
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getPostInfoForEdit(String corpNo, String untyBbsCd, Long untyBbsSno) {
|
||||
Map<String, Object> info = boardMapper.getPostInfo(buildPostParam(corpNo, untyBbsCd, untyBbsSno));
|
||||
if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
|
||||
String etcAtchNo = (String) info.get("ETC_ATCH_NO");
|
||||
String photoAtchNo = (String) info.get("PHOTO_ATCH_NO");
|
||||
if (etcAtchNo != null) info.put("etcFileList", attachService.getFileList(etcAtchNo));
|
||||
if (photoAtchNo != null) info.put("photoFileList", attachService.getFileList(photoAtchNo));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 게시물 등록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> insertPost(String corpNo, Map<String, Object> body) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Integer sno = Integer.parseInt(
|
||||
sequenceService.getNextSeqString("SX_GW0020.UNTY_BBS_SNO", 10));
|
||||
|
||||
Map<String, Object> param = new HashMap<>(body);
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
param.put("untyBbsSno", sno);
|
||||
|
||||
boardMapper.insertPost(param);
|
||||
|
||||
// 첨부 확정
|
||||
String etcAtchNo = (String) body.get("etcAtchNo");
|
||||
String photoAtchNo = (String) body.get("photoAtchNo");
|
||||
if (etcAtchNo != null) {
|
||||
param.put("etcAtchNo", etcAtchNo);
|
||||
boardMapper.updateEtcAtchNo(param);
|
||||
attachService.confirmAtchNo(etcAtchNo, "SX_GW0020.ETC_ATCH_NO");
|
||||
}
|
||||
if (photoAtchNo != null) {
|
||||
param.put("photoAtchNo", photoAtchNo);
|
||||
boardMapper.updatePhotoAtchNo(param);
|
||||
attachService.confirmAtchNo(photoAtchNo, "SX_GW0020.PHOTO_ATCH_NO");
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("untyBbsSno", sno);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 게시물 수정
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void updatePost(String corpNo, Long untyBbsSno, Map<String, Object> body) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> param = buildPostParam(corpNo, (String) body.get("untyBbsCd"), untyBbsSno);
|
||||
|
||||
Map<String, Object> existing = boardMapper.getPostInfo(param);
|
||||
if (existing == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
if (!usrId.equals(existing.get("CTUSR_ID"))) {
|
||||
throw new BizException("작성자만 수정할 수 있습니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
param.putAll(body);
|
||||
param.put("usrId", usrId);
|
||||
boardMapper.updatePost(param);
|
||||
|
||||
// 첨부 처리 (새 파일이 업로드된 경우)
|
||||
String etcAtchNo = (String) body.get("etcAtchNo");
|
||||
if (etcAtchNo != null) {
|
||||
param.put("etcAtchNo", etcAtchNo);
|
||||
boardMapper.updateEtcAtchNo(param);
|
||||
attachService.confirmAtchNo(etcAtchNo, "SX_GW0020.ETC_ATCH_NO");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 게시물 삭제
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void deletePost(String corpNo, String untyBbsCd, Long untyBbsSno) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> param = buildPostParam(corpNo, untyBbsCd, untyBbsSno);
|
||||
|
||||
Map<String, Object> info = boardMapper.getPostInfo(param);
|
||||
if (info == null) throw new BizException("게시물이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
if (!usrId.equals(info.get("CTUSR_ID"))) {
|
||||
throw new BizException("작성자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
boardMapper.deleteCommentsByPost(param);
|
||||
boardMapper.deletePost(param);
|
||||
|
||||
// 첨부파일 삭제
|
||||
String etcAtchNo = (String) info.get("ETC_ATCH_NO");
|
||||
String photoAtchNo = (String) info.get("PHOTO_ATCH_NO");
|
||||
if (etcAtchNo != null) attachService.deleteFilesByAtchNo(etcAtchNo, true);
|
||||
if (photoAtchNo != null) attachService.deleteFilesByAtchNo(photoAtchNo, true);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 댓글
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getCommentList(String corpNo, String untyBbsCd, Long untyBbsSno) {
|
||||
return boardMapper.getCommentList(buildPostParam(corpNo, untyBbsCd, untyBbsSno));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insertComment(String corpNo, String untyBbsCd, Long untyBbsSno, String cmmtCn) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Integer cmmtSno = Integer.parseInt(
|
||||
sequenceService.getNextSeqString("SX_GW0030.CMMT_SNO", 10));
|
||||
|
||||
Map<String, Object> param = buildPostParam(corpNo, untyBbsCd, untyBbsSno);
|
||||
param.put("cmmtSno", cmmtSno);
|
||||
param.put("cmmtCn", cmmtCn);
|
||||
param.put("usrId", usrId);
|
||||
|
||||
boardMapper.insertComment(param);
|
||||
boardMapper.updateCmmtCnt(param);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteComment(String corpNo, String untyBbsCd, Long untyBbsSno, Long cmmtSno) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> param = buildPostParam(corpNo, untyBbsCd, untyBbsSno);
|
||||
param.put("cmmtSno", cmmtSno);
|
||||
param.put("usrId", usrId);
|
||||
|
||||
Map<String, Object> info = boardMapper.getCommentInfo(param);
|
||||
if (info == null) throw new BizException("댓글이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
if (!usrId.equals(info.get("CMMT_CTUSR_ID"))) {
|
||||
throw new BizException("작성자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
boardMapper.deleteComment(param);
|
||||
boardMapper.updateCmmtCnt(param);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 내부 유틸
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
private Map<String, Object> buildPostParam(String corpNo, String untyBbsCd, Long untyBbsSno) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("untyBbsCd", untyBbsCd);
|
||||
p.put("untyBbsSno", untyBbsSno);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.company.gw.common.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MyBatis resultType="map" 은 DB 컬럼명을 그대로 Map 키로 반환합니다 (e.g. WORK_START_DT).
|
||||
* 프론트엔드는 camelCase(e.g. workStartDt)를 기대하므로, JSON 직렬화 시 변환합니다.
|
||||
* UPPER_CASE_SNAKE 패턴만 변환하고, 이미 camelCase인 키는 그대로 유지합니다.
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer mapKeyCamelCaseCustomizer() {
|
||||
return builder -> {
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(Map.class, new MapCamelCaseSerializer());
|
||||
builder.modules(module);
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
static class MapCamelCaseSerializer extends JsonSerializer<Map> {
|
||||
@Override
|
||||
public void serialize(Map map, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeStartObject();
|
||||
for (Object obj : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) obj;
|
||||
String key = entry.getKey() != null ? convertKey(entry.getKey().toString()) : "null";
|
||||
gen.writeFieldName(key);
|
||||
serializers.defaultSerializeValue(entry.getValue(), gen);
|
||||
}
|
||||
gen.writeEndObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* UPPER_CASE_SNAKE 패턴인 경우에만 camelCase로 변환.
|
||||
* 이미 camelCase 이거나 소문자인 키는 그대로 반환.
|
||||
*/
|
||||
private String convertKey(String key) {
|
||||
// 모두 대문자+언더스코어 패턴인 경우만 변환
|
||||
if (!key.matches("[A-Z][A-Z0-9_]*")) return key;
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean nextUpper = false;
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
char c = key.charAt(i);
|
||||
if (c == '_') {
|
||||
nextUpper = true;
|
||||
} else if (nextUpper) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
nextUpper = false;
|
||||
} else {
|
||||
result.append(Character.toLowerCase(c));
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.company.gw.common.config;
|
||||
|
||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Properties;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.company.gw.**.mapper")
|
||||
public class MyBatisConfig {
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
||||
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
|
||||
factoryBean.setDataSource(dataSource);
|
||||
factoryBean.setMapperLocations(
|
||||
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml")
|
||||
);
|
||||
|
||||
// databaseId 벤더 매핑
|
||||
// SQL Server → "mssql" (기본 구문은 databaseId 없거나 "mssql")
|
||||
// MySQL → "mariadb" (MariaDB JDBC 2.x가 MySQL로 리포트)
|
||||
// MariaDB → "mariadb" (MariaDB JDBC 3.x+)
|
||||
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
|
||||
Properties vendorProps = new Properties();
|
||||
vendorProps.setProperty("SQL Server", "mssql");
|
||||
vendorProps.setProperty("MySQL", "mariadb");
|
||||
vendorProps.setProperty("MariaDB", "mariadb");
|
||||
databaseIdProvider.setProperties(vendorProps);
|
||||
factoryBean.setDatabaseIdProvider(databaseIdProvider);
|
||||
|
||||
// application.yml의 mybatis.configuration이 수동 Bean 생성 시 무시되므로 직접 설정
|
||||
org.apache.ibatis.session.Configuration mybatisConfig = new org.apache.ibatis.session.Configuration();
|
||||
mybatisConfig.setMapUnderscoreToCamelCase(true);
|
||||
mybatisConfig.setDefaultFetchSize(100);
|
||||
mybatisConfig.setDefaultStatementTimeout(30);
|
||||
factoryBean.setConfiguration(mybatisConfig);
|
||||
|
||||
return factoryBean.getObject();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
|
||||
return new SqlSessionTemplate(sqlSessionFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.company.gw.common.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, String> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new StringRedisSerializer());
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||
try {
|
||||
// Redis 연결 가능 여부 확인
|
||||
connectionFactory.getConnection().ping();
|
||||
|
||||
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofHours(1))
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
|
||||
|
||||
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
|
||||
"code", defaultConfig.entryTtl(Duration.ofHours(6)),
|
||||
"workCd", defaultConfig.entryTtl(Duration.ofHours(6))
|
||||
);
|
||||
|
||||
log.info("CacheManager: Redis 사용");
|
||||
return RedisCacheManager.builder(connectionFactory)
|
||||
.cacheDefaults(defaultConfig)
|
||||
.withInitialCacheConfigurations(cacheConfigs)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 연결 실패 → In-Memory CacheManager 사용 ({})", e.getMessage());
|
||||
return new ConcurrentMapCacheManager("code", "workCd");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.company.gw.common.config;
|
||||
|
||||
import com.company.gw.common.filter.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.exceptionHandling(ex -> ex
|
||||
// 인증 안 된 요청 → 401 (프론트 interceptor가 refresh 시도)
|
||||
.authenticationEntryPoint((request, response, authException) -> {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.getWriter().write("{\"message\":\"Unauthorized\"}");
|
||||
})
|
||||
// 인증은 됐지만 권한 없음 → 403
|
||||
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.getWriter().write("{\"message\":\"Forbidden\"}");
|
||||
})
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.company.gw.common.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.AttachDto;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.service.AttachService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/attach")
|
||||
@RequiredArgsConstructor
|
||||
public class AttachController {
|
||||
|
||||
private final AttachService attachService;
|
||||
|
||||
/** 단일 파일 업로드 */
|
||||
@PostMapping("/upload")
|
||||
public ApiResponse<AttachDto> upload(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "atchNo", required = false) String atchNo,
|
||||
@RequestParam(value = "division", required = false) String division) {
|
||||
return ApiResponse.ok(attachService.uploadFile(file, atchNo, division));
|
||||
}
|
||||
|
||||
/** 다중 파일 업로드 */
|
||||
@PostMapping("/upload/multi")
|
||||
public ApiResponse<List<AttachDto>> uploadMulti(
|
||||
@RequestParam("files") List<MultipartFile> files,
|
||||
@RequestParam(value = "atchNo", required = false) String atchNo,
|
||||
@RequestParam(value = "division", required = false) String division) {
|
||||
return ApiResponse.ok(attachService.uploadFiles(files, atchNo, division));
|
||||
}
|
||||
|
||||
/** 에디터 이미지 업로드 */
|
||||
@PostMapping("/upload/image")
|
||||
public ApiResponse<Map<String, String>> uploadImage(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "division", required = false) String division) {
|
||||
return ApiResponse.ok(attachService.uploadEditorImage(file, division));
|
||||
}
|
||||
|
||||
/** 첨부번호로 파일 목록 조회 */
|
||||
@GetMapping("/list/{atchNo}")
|
||||
public ApiResponse<List<Map<String, Object>>> getFileList(@PathVariable String atchNo) {
|
||||
return ApiResponse.ok(attachService.getFileList(atchNo));
|
||||
}
|
||||
|
||||
/** 단일 파일 다운로드 */
|
||||
@GetMapping("/download/{atchfileNo}")
|
||||
public ResponseEntity<Resource> download(@PathVariable String atchfileNo) {
|
||||
Map<String, Object> info = attachService.getFileInfo(atchfileNo);
|
||||
File file = attachService.getPhysicalFile(atchfileNo);
|
||||
|
||||
String fileName = (String) info.get("ATCH_FILE_NM");
|
||||
String encodedName;
|
||||
try {
|
||||
encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
|
||||
.replace("+", "%20");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
encodedName = fileName;
|
||||
}
|
||||
|
||||
String contentType = (String) info.getOrDefault("ATCH_TYPE_NM",
|
||||
MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + encodedName)
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.contentLength(file.length())
|
||||
.body(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
/** 전체 파일 ZIP 다운로드 */
|
||||
@GetMapping("/download-all/{atchNo}")
|
||||
public ResponseEntity<byte[]> downloadAll(@PathVariable String atchNo) throws IOException {
|
||||
List<Map<String, Object>> fileList = attachService.getFileList(atchNo);
|
||||
if (fileList == null || fileList.isEmpty()) {
|
||||
throw new BizException("다운로드할 파일이 없습니다.", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos, StandardCharsets.UTF_8)) {
|
||||
for (Map<String, Object> info : fileList) {
|
||||
String atchfileNo = (String) info.get("ATCHFILE_NO");
|
||||
String fileName = (String) info.get("ATCH_FILE_NM");
|
||||
try {
|
||||
File physicalFile = attachService.getPhysicalFile(atchfileNo);
|
||||
zos.putNextEntry(new ZipEntry(fileName != null ? fileName : atchfileNo));
|
||||
try (FileInputStream fis = new FileInputStream(physicalFile)) {
|
||||
fis.transferTo(zos);
|
||||
}
|
||||
zos.closeEntry();
|
||||
} catch (Exception e) {
|
||||
// 개별 파일 오류 시 건너뜀
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String encodedName = URLEncoder.encode("첨부파일.zip", StandardCharsets.UTF_8)
|
||||
.replace("+", "%20");
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + encodedName)
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(baos.toByteArray());
|
||||
}
|
||||
|
||||
/** 파일 삭제 */
|
||||
@DeleteMapping("/{atchfileNo}")
|
||||
public ApiResponse<Void> delete(@PathVariable String atchfileNo) {
|
||||
attachService.deleteFile(atchfileNo, false);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.company.gw.common.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.dto.LoginRequestDto;
|
||||
import com.company.gw.common.dto.LoginResponseDto;
|
||||
import com.company.gw.common.dto.RefreshRequestDto;
|
||||
import com.company.gw.common.service.AuthService;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<ApiResponse<LoginResponseDto>> login(
|
||||
@Valid @RequestBody LoginRequestDto req) {
|
||||
LoginResponseDto result = authService.login(req);
|
||||
return ResponseEntity.ok(ApiResponse.ok(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<ApiResponse<Void>> logout(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
authService.logout(userDetails.getUsername());
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token 재발급
|
||||
* POST /api/auth/refresh
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<ApiResponse<String>> refresh(
|
||||
@Valid @RequestBody RefreshRequestDto req) {
|
||||
LoginResponseDto.UserInfo userInfo = authService.refreshToken(req.getRefreshToken());
|
||||
String roleCode = (userInfo.getRoles() != null && !userInfo.getRoles().isEmpty())
|
||||
? userInfo.getRoles().get(0) : "USER";
|
||||
String newAccessToken = jwtUtil.generateAccessToken(userInfo.getUsrId(), userInfo.getCorpNo(), roleCode);
|
||||
return ResponseEntity.ok(ApiResponse.ok(newAccessToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 로그인 사용자 정보
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<ApiResponse<String>> me(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(userDetails.getUsername()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
* POST /api/auth/change-pw
|
||||
*/
|
||||
@PostMapping("/change-pw")
|
||||
public ResponseEntity<ApiResponse<Void>> changePassword(
|
||||
@AuthenticationPrincipal CurrentUser currentUser,
|
||||
@RequestBody Map<String, String> body) {
|
||||
authService.changePassword(
|
||||
currentUser.getUsrId(),
|
||||
body.get("oldPw"),
|
||||
body.get("newPw")
|
||||
);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.company.gw.common.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.service.CodeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/code")
|
||||
@RequiredArgsConstructor
|
||||
public class CodeController {
|
||||
|
||||
private final CodeService codeService;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 공통코드 목록 조회
|
||||
* GET /api/code?commClCd=SX001&useYn=Y
|
||||
*/
|
||||
@GetMapping
|
||||
public ApiResponse<List<Map<String, Object>>> getCodeList(
|
||||
@RequestParam String commClCd,
|
||||
@RequestParam(defaultValue = "") String useYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(codeService.getCodeList(currentUser.getCorpNo(), commClCd, useYn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통코드 전체 정보 조회 (PROP_CD 포함)
|
||||
*/
|
||||
@GetMapping("/full")
|
||||
public ApiResponse<List<Map<String, Object>>> getCodeListFull(
|
||||
@RequestParam String commClCd,
|
||||
@RequestParam(defaultValue = "") String useYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(codeService.getCodeListFull(currentUser.getCorpNo(), commClCd, useYn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 직원 검색 (결재자 팝업 등)
|
||||
* GET /api/code/users?searchText=홍&apprOnly=Y
|
||||
*/
|
||||
@GetMapping("/users")
|
||||
public ApiResponse<List<Map<String, Object>>> searchUser(
|
||||
@RequestParam(defaultValue = "") String searchText,
|
||||
@RequestParam(defaultValue = "N") String apprOnly,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(codeService.searchUser(
|
||||
currentUser.getCorpNo(), searchText, "Y".equals(apprOnly)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 근무코드 목록 조회
|
||||
*/
|
||||
@GetMapping("/workcd")
|
||||
public ApiResponse<List<Map<String, Object>>> getWorkCdList(
|
||||
@RequestParam(defaultValue = "") String useYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(codeService.getWorkCdList(currentUser.getCorpNo(), useYn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 초기화 (코드 변경 후 호출)
|
||||
*/
|
||||
@PostMapping("/cache/evict")
|
||||
public ApiResponse<Void> evictCache() {
|
||||
Objects.requireNonNull(cacheManager.getCache("code")).clear();
|
||||
Objects.requireNonNull(cacheManager.getCache("workCd")).clear();
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.company.gw.common.controller;
|
||||
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.ExcelUtil;
|
||||
import com.company.gw.tam.mapper.TamMapper;
|
||||
import com.company.gw.wtime.mapper.WtimeMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Excel 내보내기 공통 컨트롤러
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/excel")
|
||||
@RequiredArgsConstructor
|
||||
public class ExcelController {
|
||||
|
||||
private final WtimeMapper wtimeMapper;
|
||||
private final TamMapper tamMapper;
|
||||
|
||||
/** 근무시간 목록 엑셀 */
|
||||
@GetMapping("/wtime")
|
||||
public ResponseEntity<byte[]> wtimeExcel(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser cu) throws IOException {
|
||||
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
setDefault(params, "staYmd", "00000000");
|
||||
setDefault(params, "endYmd", "99999999");
|
||||
setDefault(params, "includeRetireYn", "N");
|
||||
setDefault(params, "includeWorkYn", "N");
|
||||
|
||||
List<Map<String, Object>> data = wtimeMapper.getWtimeList(params);
|
||||
|
||||
return ExcelUtil.toExcel("근무시간",
|
||||
new String[]{"날짜", "이름", "팀", "직위", "근무코드", "출근", "퇴근", "총근무(분)", "지각(분)", "OT(분)", "결근"},
|
||||
new String[]{"WORK_PLAN_YYMMDD", "USR_NM", "TEAM_CD", "DUTY_CD",
|
||||
"REAL_WORK_CD", "WORK_START_DT", "WORK_END_DT",
|
||||
"TOT_WORK_MIN", "LATE_MIN", "OT_WORK_MIN", "ABTI_YN"},
|
||||
data);
|
||||
}
|
||||
|
||||
/** 월별 근무통계 엑셀 */
|
||||
@GetMapping("/wstat")
|
||||
public ResponseEntity<byte[]> wstatExcel(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser cu) throws IOException {
|
||||
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
setDefault(params, "staYmd", "00000000");
|
||||
setDefault(params, "endYmd", "99999999");
|
||||
setDefault(params, "includeRetireYn", "N");
|
||||
|
||||
List<Map<String, Object>> data = wtimeMapper.getWstatList(params);
|
||||
|
||||
return ExcelUtil.toExcel("근무통계",
|
||||
new String[]{"이름", "팀", "직위", "연차", "근무일", "결근", "지각일", "조퇴일", "총근무(분)", "지각(분)", "OT(분)"},
|
||||
new String[]{"USR_NM", "TEAM_CD", "DUTY_CD", "YYVCT_CNT",
|
||||
"WORK_DCNT", "ABTI_DCNT", "LATE_DCNT", "SKIPOFF_DCNT",
|
||||
"TOT_WORK_MIN", "LATE_MIN", "OT_WORK_MIN"},
|
||||
data);
|
||||
}
|
||||
|
||||
/** 결재 신청 목록 엑셀 (TAM0020) */
|
||||
@GetMapping("/tam-apvreq")
|
||||
public ResponseEntity<byte[]> tamApvreqExcel(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser cu) throws IOException {
|
||||
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
params.put("usrId", cu.getUsrId());
|
||||
setDefault(params, "staYmd", "00000000");
|
||||
setDefault(params, "endYmd", "99999999");
|
||||
setDefault(params, "pageNo", 1);
|
||||
setDefault(params, "pageSize", 10000);
|
||||
|
||||
List<Map<String, Object>> data = tamMapper.getApvreqList(params);
|
||||
|
||||
return ExcelUtil.toExcel("결재신청목록",
|
||||
new String[]{"문서ID", "상태", "종류", "상신일", "완료일"},
|
||||
new String[]{"APRVL_DOC_ID", "APRVL_STUS_CD", "APRVL_KIND_CD", "OFFER_DT", "CMPL_DT"},
|
||||
data);
|
||||
}
|
||||
|
||||
/** 근태 현황 엑셀 (TAM0040) */
|
||||
@GetMapping("/tam-status")
|
||||
public ResponseEntity<byte[]> tamStatusExcel(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser cu) throws IOException {
|
||||
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
setDefault(params, "staYmd", "00000000");
|
||||
setDefault(params, "endYmd", "99999999");
|
||||
setDefault(params, "includeRetireYn", "N");
|
||||
|
||||
List<Map<String, Object>> data = tamMapper.getTamStatusList(params);
|
||||
|
||||
return ExcelUtil.toExcel("근태현황",
|
||||
new String[]{"이름", "팀", "직위", "연차", "결재건수"},
|
||||
new String[]{"USR_NM", "TEAM_CD", "DUTY_CD", "YYVCT_CNT", "APRVL_CNT"},
|
||||
data);
|
||||
}
|
||||
|
||||
private void setDefault(Map<String, Object> params, String key, Object defaultVal) {
|
||||
if (!params.containsKey(key) || params.get(key) == null) {
|
||||
params.put(key, defaultVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private boolean success;
|
||||
private T data;
|
||||
private String message;
|
||||
private PaginationInfo pagination;
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.success(true)
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data, PaginationInfo pagination) {
|
||||
return ApiResponse.<T>builder()
|
||||
.success(true)
|
||||
.data(data)
|
||||
.pagination(pagination)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> fail(String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.success(false)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class PaginationInfo {
|
||||
private int page;
|
||||
private int size;
|
||||
private long total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class AttachDto {
|
||||
private String atchfileNo;
|
||||
private String atchNo;
|
||||
private String atchFilePathNm;
|
||||
private String atchFileNm;
|
||||
private String atchTypeNm;
|
||||
private Long atchFileMg;
|
||||
private String tblColNm;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class ControllerRoleDto {
|
||||
private String controller;
|
||||
private String role;
|
||||
private String funcAuthCn;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* 기존 Grails의 SS_CORP_NO, SS_USER_ID, ROLE_CD를 대체하는 현재 로그인 사용자 정보.
|
||||
* JWT 클레임에서 추출하여 컨트롤러/서비스에서 사용.
|
||||
* Principal 구현 → auth.getName()이 usrId를 반환하도록 보장.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class CurrentUser implements Principal {
|
||||
private final String usrId;
|
||||
private final String corpNo;
|
||||
private final String roleCode;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return usrId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class LoginRequestDto {
|
||||
|
||||
@NotBlank(message = "로그인 아이디를 입력하세요.")
|
||||
private String loginId;
|
||||
|
||||
@NotBlank(message = "비밀번호를 입력하세요.")
|
||||
private String loginPw;
|
||||
|
||||
@NotBlank(message = "권한을 선택하세요.")
|
||||
private String menuAuthCd;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class LoginResponseDto {
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private UserInfo userInfo;
|
||||
private List<MenuDto> menuList;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class UserInfo {
|
||||
private String usrId;
|
||||
private String usrNm;
|
||||
private String loginId;
|
||||
private String corpNo;
|
||||
private String dutyCd;
|
||||
private String roleCode; // 선택한 MENU_AUTH_CD
|
||||
private List<String> roles; // 보유 전체 role 목록
|
||||
}
|
||||
}
|
||||
18
backend/src/main/java/com/company/gw/common/dto/MenuDto.java
Normal file
18
backend/src/main/java/com/company/gw/common/dto/MenuDto.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class MenuDto {
|
||||
private String menuNo;
|
||||
private String upperMenuNo;
|
||||
private String menuNm;
|
||||
private String url;
|
||||
private String rm;
|
||||
private String menuProp;
|
||||
private Integer menuOrdr;
|
||||
private String menuUseYn;
|
||||
private Integer lvl;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class RefreshRequestDto {
|
||||
|
||||
@NotBlank(message = "refreshToken이 필요합니다.")
|
||||
private String refreshToken;
|
||||
}
|
||||
19
backend/src/main/java/com/company/gw/common/dto/UserDto.java
Normal file
19
backend/src/main/java/com/company/gw/common/dto/UserDto.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.company.gw.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class UserDto {
|
||||
private String corpNo;
|
||||
private String usrId;
|
||||
private String usrNm;
|
||||
private String loginId;
|
||||
@JsonIgnore
|
||||
private String pw;
|
||||
private String dutyCd;
|
||||
@JsonIgnore
|
||||
private String retirementDate;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.company.gw.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@Getter
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
private final HttpStatus status;
|
||||
|
||||
public BizException(String message) {
|
||||
super(message);
|
||||
this.status = HttpStatus.BAD_REQUEST;
|
||||
}
|
||||
|
||||
public BizException(String message, HttpStatus status) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.company.gw.common.exception;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleBizException(BizException e) {
|
||||
log.warn("BizException: {}", e.getMessage());
|
||||
return ResponseEntity
|
||||
.status(e.getStatus())
|
||||
.body(ApiResponse.fail(e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
|
||||
public ResponseEntity<ApiResponse<Void>> handleValidationException(BindException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
|
||||
.findFirst()
|
||||
.orElse("입력값이 올바르지 않습니다.");
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.fail(message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
|
||||
log.error("Unhandled exception", e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.fail("서버 오류가 발생했습니다."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.company.gw.common.filter;
|
||||
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtUtil.isValid(token)) {
|
||||
try {
|
||||
String usrId = jwtUtil.getUserId(token);
|
||||
String corpNo = jwtUtil.getCorpNo(token);
|
||||
String roleCode = jwtUtil.getRoleCode(token);
|
||||
|
||||
if (StringUtils.hasText(usrId)) {
|
||||
CurrentUser currentUser = CurrentUser.builder()
|
||||
.usrId(usrId)
|
||||
.corpNo(corpNo)
|
||||
.roleCode(roleCode)
|
||||
.build();
|
||||
|
||||
var authorities = StringUtils.hasText(roleCode)
|
||||
? List.of(new SimpleGrantedAuthority("ROLE_" + roleCode))
|
||||
: List.<SimpleGrantedAuthority>of();
|
||||
|
||||
var authentication = new UsernamePasswordAuthenticationToken(
|
||||
currentUser, null, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT 인증 실패 - 인증 없이 계속 진행: {}", e.getMessage());
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface AttachMapper {
|
||||
|
||||
Map<String, Object> getFileInfo(String atchfileNo);
|
||||
|
||||
List<Map<String, Object>> getFileInfoListByAtchNo(String atchNo);
|
||||
|
||||
void insertFileInfo(Map<String, Object> param);
|
||||
|
||||
void deleteFileInfo(String atchfileNo);
|
||||
|
||||
void updateTblColNmByAtchNo(Map<String, Object> param);
|
||||
|
||||
void updateTblColNmByAtchfileNo(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> getNeedDeleteFileList();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface CodeMapper {
|
||||
|
||||
List<Map<String, Object>> getCodeList(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> getCodeListFull(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> searchUser(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> getWorkCdList(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface CommonMapper {
|
||||
|
||||
void increaseSequence(@Param("sequenceNm") String sequenceNm);
|
||||
|
||||
Long getSequenceVal(@Param("sequenceNm") String sequenceNm);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import com.company.gw.common.dto.ControllerRoleDto;
|
||||
import com.company.gw.common.dto.MenuDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface MenuMapper {
|
||||
|
||||
List<MenuDto> getMenuList(@Param("corpNo") String corpNo,
|
||||
@Param("menuAuthCd") String menuAuthCd);
|
||||
|
||||
List<ControllerRoleDto> getControllerRoleList(@Param("corpNo") String corpNo);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface SecurityMapper {
|
||||
|
||||
List<String> getUserRoleList(@Param("corpNo") String corpNo,
|
||||
@Param("usrId") String usrId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.company.gw.common.mapper;
|
||||
|
||||
import com.company.gw.common.dto.UserDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
|
||||
UserDto getUserInfo(@Param("usrId") String usrId);
|
||||
|
||||
UserDto getUserInfoWithPwd(@Param("loginId") String loginId);
|
||||
|
||||
int updatePassword(@Param("usrId") String usrId, @Param("newPw") String newPw);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.company.gw.common.service;
|
||||
|
||||
import com.company.gw.common.dto.AttachDto;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.mapper.AttachMapper;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AttachService {
|
||||
|
||||
private static final List<String> NOT_ALLOWED_EXT =
|
||||
Arrays.asList("php", "jsp", "asp", "gsp", "sh", "exe", "bat");
|
||||
|
||||
private static final List<String> IMAGE_EXT =
|
||||
Arrays.asList("jpg", "jpeg", "png", "gif", "tif", "tiff", "bmp", "webp");
|
||||
|
||||
private static final long MAX_FILE_SIZE = 30L * 1024 * 1024; // 30MB
|
||||
|
||||
private final AttachMapper attachMapper;
|
||||
|
||||
@Value("${file.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 업로드
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public AttachDto uploadFile(MultipartFile file, String atchNo, String division) {
|
||||
validateFile(file);
|
||||
|
||||
if (!org.springframework.util.StringUtils.hasText(atchNo)) {
|
||||
atchNo = UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
String ext = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase();
|
||||
String atchfileNo = UUID.randomUUID().toString().replace("-", "");
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String relativePath = (division != null ? division : "common") + "/" + today + "/" + atchfileNo + "." + ext;
|
||||
|
||||
File dest = new File(uploadPath, relativePath);
|
||||
dest.getParentFile().mkdirs();
|
||||
try {
|
||||
file.transferTo(dest);
|
||||
} catch (IOException e) {
|
||||
throw new BizException("파일 저장 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("atchfileNo", atchfileNo);
|
||||
param.put("atchNo", atchNo);
|
||||
param.put("atchFilePathNm", relativePath);
|
||||
param.put("atchFileNm", file.getOriginalFilename());
|
||||
param.put("atchTypeNm", file.getContentType());
|
||||
param.put("atchFileMg", file.getSize());
|
||||
param.put("tblColNm", null);
|
||||
param.put("rgstrId", SecurityUtil.getUsrId());
|
||||
|
||||
attachMapper.insertFileInfo(param);
|
||||
|
||||
return AttachDto.builder()
|
||||
.atchfileNo(atchfileNo)
|
||||
.atchNo(atchNo)
|
||||
.atchFilePathNm(relativePath)
|
||||
.atchFileNm(file.getOriginalFilename())
|
||||
.atchTypeNm(file.getContentType())
|
||||
.atchFileMg(file.getSize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<AttachDto> uploadFiles(List<MultipartFile> files, String atchNo, String division) {
|
||||
if (!org.springframework.util.StringUtils.hasText(atchNo)) {
|
||||
atchNo = UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
List<AttachDto> result = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
if (file != null && !file.isEmpty()) {
|
||||
result.add(uploadFile(file, atchNo, division));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 이미지 에디터용 업로드 (DB 저장 없이 파일만 저장)
|
||||
public Map<String, String> uploadEditorImage(MultipartFile file, String division) {
|
||||
String ext = FilenameUtils.getExtension(
|
||||
Objects.requireNonNull(file.getOriginalFilename())).toLowerCase();
|
||||
if (!IMAGE_EXT.contains(ext)) {
|
||||
throw new BizException("이미지 파일만 업로드할 수 있습니다.");
|
||||
}
|
||||
if (file.getSize() > MAX_FILE_SIZE) {
|
||||
throw new BizException("파일 크기가 초과되었습니다. (최대 30MB)");
|
||||
}
|
||||
|
||||
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String fileId = UUID.randomUUID().toString().replace("-", "");
|
||||
String relativePath = (division != null ? division : "editor") + "/" + today + "/" + fileId + "." + ext;
|
||||
|
||||
File dest = new File(uploadPath + "/images", relativePath);
|
||||
dest.getParentFile().mkdirs();
|
||||
try {
|
||||
file.transferTo(dest);
|
||||
} catch (IOException e) {
|
||||
throw new BizException("이미지 저장 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("url", "/api/attach/image/" + relativePath.replace("/", "_") + "." + ext);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 조회
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> getFileInfo(String atchfileNo) {
|
||||
Map<String, Object> info = attachMapper.getFileInfo(atchfileNo);
|
||||
if (info == null) {
|
||||
throw new BizException("파일 정보가 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getFileList(String atchNo) {
|
||||
return attachMapper.getFileInfoListByAtchNo(atchNo);
|
||||
}
|
||||
|
||||
public File getPhysicalFile(String atchfileNo) {
|
||||
Map<String, Object> info = getFileInfo(atchfileNo);
|
||||
File file = new File(uploadPath, (String) info.get("ATCH_FILE_PATH_NM"));
|
||||
if (!file.exists()) {
|
||||
throw new BizException("파일이 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 삭제
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void deleteFile(String atchfileNo, boolean forceDelete) {
|
||||
Map<String, Object> info = getFileInfo(atchfileNo);
|
||||
if (!forceDelete) {
|
||||
String rgstrId = (String) info.get("RGSTR_ID");
|
||||
if (!SecurityUtil.getUsrId().equals(rgstrId)) {
|
||||
throw new BizException("등록자만 삭제할 수 있습니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
attachMapper.deleteFileInfo(atchfileNo);
|
||||
// 물리 파일 삭제 (DB 성공 후)
|
||||
new File(uploadPath, (String) info.get("ATCH_FILE_PATH_NM")).delete();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteFilesByAtchNo(String atchNo, boolean forceDelete) {
|
||||
List<Map<String, Object>> list = attachMapper.getFileInfoListByAtchNo(atchNo);
|
||||
for (Map<String, Object> info : list) {
|
||||
deleteFile((String) info.get("ATCHFILE_NO"), forceDelete);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TBL_COL_NM 확정 처리 (게시판/결재 저장 완료 후 호출)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void confirmAtchNo(String atchNo, String tblColNm) {
|
||||
if (!org.springframework.util.StringUtils.hasText(atchNo)) return;
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("atchNo", atchNo);
|
||||
param.put("tblColNm", tblColNm);
|
||||
attachMapper.updateTblColNmByAtchNo(param);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 미확정 파일 정기 삭제 (6시간 이상 TBL_COL_NM 없는 파일)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Scheduled(cron = "0 0 3 * * *") // 매일 새벽 3시
|
||||
@Transactional
|
||||
public void cleanUpUnnecessaryFiles() {
|
||||
List<Map<String, Object>> list = attachMapper.getNeedDeleteFileList();
|
||||
for (Map<String, Object> item : list) {
|
||||
try {
|
||||
attachMapper.deleteFileInfo((String) item.get("ATCHFILE_NO"));
|
||||
new File(uploadPath, (String) item.get("ATCH_FILE_PATH_NM")).delete();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 검증
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
private void validateFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BizException("파일이 없습니다.");
|
||||
}
|
||||
String ext = FilenameUtils.getExtension(
|
||||
Objects.requireNonNull(file.getOriginalFilename())).toLowerCase();
|
||||
if (NOT_ALLOWED_EXT.contains(ext)) {
|
||||
throw new BizException("업로드할 수 없는 확장자입니다. (" + ext + ")");
|
||||
}
|
||||
if (file.getSize() > MAX_FILE_SIZE) {
|
||||
throw new BizException("파일 크기가 초과되었습니다. (최대 30MB)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.company.gw.common.service;
|
||||
|
||||
import com.company.gw.common.dto.*;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.mapper.MenuMapper;
|
||||
import com.company.gw.common.mapper.SecurityMapper;
|
||||
import com.company.gw.common.mapper.UserMapper;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import com.company.gw.common.util.PasswordUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthService {
|
||||
|
||||
private static final String ROLE_ADMIN = "BIDS_0001";
|
||||
private static final String ROLE_FEDEX = "FEDE";
|
||||
private static final String ROLE_USER = "USER";
|
||||
private static final String REFRESH_TOKEN_PREFIX = "refresh:";
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final SecurityMapper securityMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
/**
|
||||
* 로그인
|
||||
* 기존 IndexController.do_login() 로직과 동일
|
||||
*/
|
||||
public LoginResponseDto login(LoginRequestDto req) {
|
||||
|
||||
// 1. 사용자 조회 (PW 포함)
|
||||
UserDto user = userMapper.getUserInfoWithPwd(req.getLoginId());
|
||||
if (user == null) {
|
||||
throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요.");
|
||||
}
|
||||
|
||||
// 2. 비밀번호 검증 (SHA-256 + Base64)
|
||||
if (!PasswordUtil.matches(req.getLoginPw(), user.getPw())) {
|
||||
throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요.");
|
||||
}
|
||||
|
||||
// 3. 퇴직자 체크
|
||||
if (StringUtils.hasText(user.getRetirementDate())) {
|
||||
throw new BizException("로그인 아이디 또는 비밀번호를 확인하세요.");
|
||||
}
|
||||
|
||||
// 4. 보유 롤 조회
|
||||
List<String> roles = securityMapper.getUserRoleList(user.getCorpNo(), user.getUsrId());
|
||||
|
||||
// 5. 선택한 MENU_AUTH_CD 보유 여부 검증
|
||||
// 구 시스템(SecurityService.groovy) 로직과 동일:
|
||||
// - FEDE 사용자이거나, USER 이외의 권한을 선택한 경우에만 DB 체크
|
||||
// - USER 권한은 FEDE 사용자가 아니면 누구나 사용 가능 (기본값)
|
||||
boolean isFedexUser = roles.contains(ROLE_FEDEX);
|
||||
String menuAuthCd = req.getMenuAuthCd();
|
||||
if (isFedexUser || !ROLE_USER.equals(menuAuthCd)) {
|
||||
if (!roles.contains(menuAuthCd)) {
|
||||
throw new BizException("해당 메뉴 권한이 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. JWT 발급 (usrId + corpNo + roleCode 포함)
|
||||
String accessToken = jwtUtil.generateAccessToken(user.getUsrId(), user.getCorpNo(), menuAuthCd);
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getUsrId());
|
||||
|
||||
// 7. Redis에 Refresh Token 저장 (7일) — Redis 미사용 환경에서는 건너뜀
|
||||
try {
|
||||
redisTemplate.opsForValue().set(
|
||||
REFRESH_TOKEN_PREFIX + user.getUsrId(),
|
||||
refreshToken,
|
||||
7, TimeUnit.DAYS
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 미사용 환경: Refresh Token 저장 건너뜀 ({})", e.getMessage());
|
||||
}
|
||||
|
||||
// 8. 메뉴 조회 - 선택한 역할 코드로 SX_CO0090 기반 필터링
|
||||
List<MenuDto> menuList = menuMapper.getMenuList(user.getCorpNo(), menuAuthCd);
|
||||
|
||||
return LoginResponseDto.builder()
|
||||
.accessToken(accessToken)
|
||||
.refreshToken(refreshToken)
|
||||
.userInfo(LoginResponseDto.UserInfo.builder()
|
||||
.usrId(user.getUsrId())
|
||||
.usrNm(user.getUsrNm())
|
||||
.loginId(user.getLoginId())
|
||||
.corpNo(user.getCorpNo())
|
||||
.dutyCd(user.getDutyCd())
|
||||
.roleCode(menuAuthCd)
|
||||
.roles(roles)
|
||||
.build())
|
||||
.menuList(menuList)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token 재발급 (Refresh Token 검증)
|
||||
*/
|
||||
public LoginResponseDto.UserInfo refreshToken(String refreshToken) {
|
||||
if (!jwtUtil.isValid(refreshToken)) {
|
||||
throw new BizException("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
String usrId = jwtUtil.getUserId(refreshToken);
|
||||
try {
|
||||
String stored = redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + usrId);
|
||||
if (stored != null && !refreshToken.equals(stored)) {
|
||||
throw new BizException("만료된 토큰입니다. 다시 로그인하세요.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// stored == null: Redis 미사용 환경 → JWT 유효성만으로 통과
|
||||
} catch (BizException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 미사용 환경: Refresh Token 검증 건너뜀 ({})", e.getMessage());
|
||||
}
|
||||
|
||||
// 사용자 정보 재조회
|
||||
UserDto user = userMapper.getUserInfo(usrId);
|
||||
if (user == null) {
|
||||
throw new BizException("사용자 정보를 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
List<String> roles = securityMapper.getUserRoleList(user.getCorpNo(), usrId);
|
||||
|
||||
return LoginResponseDto.UserInfo.builder()
|
||||
.usrId(user.getUsrId())
|
||||
.usrNm(user.getUsrNm())
|
||||
.loginId(user.getLoginId())
|
||||
.corpNo(user.getCorpNo())
|
||||
.dutyCd(user.getDutyCd())
|
||||
.roles(roles)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 - Redis에서 Refresh Token 삭제
|
||||
*/
|
||||
public void logout(String usrId) {
|
||||
try {
|
||||
redisTemplate.delete(REFRESH_TOKEN_PREFIX + usrId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 미사용 환경: 로그아웃 토큰 삭제 건너뜀 ({})", e.getMessage());
|
||||
}
|
||||
log.debug("로그아웃 처리 완료: {}", usrId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void changePassword(String usrId, String oldPw, String newPw) {
|
||||
UserDto user = userMapper.getUserInfo(usrId);
|
||||
if (user == null) {
|
||||
throw new BizException("사용자 정보를 찾을 수 없습니다.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
// 현재 비밀번호 확인 (loginId로 재조회하여 PW 포함)
|
||||
UserDto userWithPw = userMapper.getUserInfoWithPwd(user.getLoginId());
|
||||
if (userWithPw == null || !PasswordUtil.matches(oldPw, userWithPw.getPw())) {
|
||||
throw new BizException("현재 비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
if (!StringUtils.hasText(newPw) || newPw.length() < 4) {
|
||||
throw new BizException("새 비밀번호는 4자 이상이어야 합니다.");
|
||||
}
|
||||
userMapper.updatePassword(usrId, PasswordUtil.encode(newPw));
|
||||
try {
|
||||
redisTemplate.delete(REFRESH_TOKEN_PREFIX + usrId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Redis 미사용 환경: 비밀번호 변경 후 토큰 삭제 건너뜀 ({})", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 여부
|
||||
*/
|
||||
public boolean isAdmin(List<String> roles) {
|
||||
return roles.contains(ROLE_ADMIN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.company.gw.common.service;
|
||||
|
||||
import com.company.gw.common.mapper.CodeMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 공통코드 조회 서비스
|
||||
* - @Cacheable("code") 로 Redis 캐시 적용 (기본 TTL은 RedisConfig에서 설정)
|
||||
* - 공통코드 변경 시 /api/code/cache/evict 호출로 캐시 초기화
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CodeService {
|
||||
|
||||
private final CodeMapper codeMapper;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 공통코드 목록 (드롭다운용)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Cacheable(value = "code", key = "#corpNo + ':' + #commClCd + ':' + #useYn")
|
||||
public List<Map<String, Object>> getCodeList(String corpNo, String commClCd, String useYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("commClCd", commClCd);
|
||||
p.put("useYn", useYn);
|
||||
return codeMapper.getCodeList(p);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getCodeListFull(String corpNo, String commClCd, String useYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("commClCd", commClCd);
|
||||
p.put("useYn", useYn);
|
||||
return codeMapper.getCodeListFull(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 직원 검색 (결재자 선택 팝업 등)
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> searchUser(String corpNo, String searchText, boolean apprOnly) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("searchText", searchText);
|
||||
p.put("apprOnly", apprOnly ? "Y" : "N");
|
||||
return codeMapper.searchUser(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 근무코드 목록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Cacheable(value = "workCd", key = "#corpNo + ':' + #useYn")
|
||||
public List<Map<String, Object>> getWorkCdList(String corpNo, String useYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("useYn", useYn);
|
||||
return codeMapper.getWorkCdList(p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.company.gw.common.service;
|
||||
|
||||
import com.company.gw.common.dto.UserDto;
|
||||
import com.company.gw.common.mapper.SecurityMapper;
|
||||
import com.company.gw.common.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final SecurityMapper securityMapper;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String usrId) throws UsernameNotFoundException {
|
||||
UserDto user = userMapper.getUserInfo(usrId);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + usrId);
|
||||
}
|
||||
|
||||
List<String> roles = securityMapper.getUserRoleList(user.getCorpNo(), usrId);
|
||||
List<SimpleGrantedAuthority> authorities = roles.stream()
|
||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||
.toList();
|
||||
|
||||
return User.builder()
|
||||
.username(usrId)
|
||||
.password("") // JWT 방식이므로 password 불필요
|
||||
.authorities(authorities)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.company.gw.common.service;
|
||||
|
||||
import com.company.gw.common.mapper.CommonMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 기존 CommonService.getNextSeqString() 대체.
|
||||
* SX_CO0060 테이블의 MERGE 기반 시퀀스.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SequenceService {
|
||||
|
||||
private final CommonMapper commonMapper;
|
||||
|
||||
@Transactional
|
||||
public String getNextSeqString(String sequenceNm, int padLength) {
|
||||
commonMapper.increaseSequence(sequenceNm);
|
||||
Long val = commonMapper.getSequenceVal(sequenceNm);
|
||||
return String.format("%0" + padLength + "d", val);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.company.gw.common.util;
|
||||
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Excel 내보내기 유틸리티
|
||||
*/
|
||||
public class ExcelUtil {
|
||||
|
||||
private ExcelUtil() {}
|
||||
|
||||
/**
|
||||
* Map 리스트를 Excel로 변환하여 ResponseEntity 반환
|
||||
* @param fileName 다운로드 파일명 (확장자 없이)
|
||||
* @param headers 컬럼 헤더 배열
|
||||
* @param keys Map에서 읽을 키 배열 (headers와 1:1 대응)
|
||||
* @param data 데이터 리스트
|
||||
*/
|
||||
public static ResponseEntity<byte[]> toExcel(String fileName,
|
||||
String[] headers,
|
||||
String[] keys,
|
||||
List<Map<String, Object>> data) throws IOException {
|
||||
try (Workbook wb = new XSSFWorkbook()) {
|
||||
Sheet sheet = wb.createSheet("Sheet1");
|
||||
|
||||
// 헤더 스타일
|
||||
CellStyle headerStyle = wb.createCellStyle();
|
||||
Font headerFont = wb.createFont();
|
||||
headerFont.setBold(true);
|
||||
headerStyle.setFont(headerFont);
|
||||
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerStyle.setBorderBottom(BorderStyle.THIN);
|
||||
|
||||
// 헤더 행
|
||||
Row headerRow = sheet.createRow(0);
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers[i]);
|
||||
cell.setCellStyle(headerStyle);
|
||||
sheet.setColumnWidth(i, 4000);
|
||||
}
|
||||
|
||||
// 데이터 행
|
||||
int rowIdx = 1;
|
||||
for (Map<String, Object> row : data) {
|
||||
Row dataRow = sheet.createRow(rowIdx++);
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
Cell cell = dataRow.createCell(i);
|
||||
Object val = row.get(keys[i]);
|
||||
if (val == null) {
|
||||
cell.setCellValue("");
|
||||
} else if (val instanceof Number) {
|
||||
cell.setCellValue(((Number) val).doubleValue());
|
||||
} else {
|
||||
cell.setCellValue(val.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
wb.write(baos);
|
||||
|
||||
String encodedName = URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8)
|
||||
.replace("+", "%20");
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + encodedName)
|
||||
.contentType(MediaType.parseMediaType(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.body(baos.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.company.gw.common.util;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private static final String CLAIM_CORP_NO = "corpNo";
|
||||
private static final String CLAIM_ROLE_CODE = "roleCode";
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long accessTokenExpiry;
|
||||
private final long refreshTokenExpiry;
|
||||
|
||||
public JwtUtil(
|
||||
@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.access-token-expiry}") long accessTokenExpiry,
|
||||
@Value("${jwt.refresh-token-expiry}") long refreshTokenExpiry) {
|
||||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.accessTokenExpiry = accessTokenExpiry;
|
||||
this.refreshTokenExpiry = refreshTokenExpiry;
|
||||
}
|
||||
|
||||
public String generateAccessToken(String usrId, String corpNo, String roleCode) {
|
||||
return Jwts.builder()
|
||||
.subject(usrId)
|
||||
.claim(CLAIM_CORP_NO, corpNo)
|
||||
.claim(CLAIM_ROLE_CODE, roleCode)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
|
||||
.signWith(secretKey)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(String usrId) {
|
||||
return Jwts.builder()
|
||||
.subject(usrId)
|
||||
.issuedAt(new Date())
|
||||
.expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry))
|
||||
.signWith(secretKey)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String getUserId(String token) {
|
||||
return getClaims(token).getSubject();
|
||||
}
|
||||
|
||||
public String getCorpNo(String token) {
|
||||
return (String) getClaims(token).get(CLAIM_CORP_NO);
|
||||
}
|
||||
|
||||
public String getRoleCode(String token) {
|
||||
return (String) getClaims(token).get(CLAIM_ROLE_CODE);
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
try {
|
||||
getClaims(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
log.debug("Invalid JWT token: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Claims getClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.company.gw.common.util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 기존 Grails EncryptUtil.get_enc_password() 와 동일한 방식
|
||||
* SHA-256 → Base64 인코딩
|
||||
* 기존 Oracle DB의 PW 컬럼 값과 호환 필수
|
||||
*/
|
||||
public class PasswordUtil {
|
||||
|
||||
private PasswordUtil() {}
|
||||
|
||||
public static String encode(String rawPassword) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(rawPassword.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 알고리즘을 찾을 수 없습니다.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matches(String rawPassword, String encodedPassword) {
|
||||
return encode(rawPassword).equals(encodedPassword);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.company.gw.common.util;
|
||||
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* JWT에서 현재 사용자 정보를 추출하는 유틸.
|
||||
* 기존 CommonUtil.getLoginCorpNo(), getLoginUserID() 역할.
|
||||
*/
|
||||
public class SecurityUtil {
|
||||
|
||||
private SecurityUtil() {}
|
||||
|
||||
public static CurrentUser getCurrentUser(JwtUtil jwtUtil) {
|
||||
String token = resolveToken();
|
||||
if (!StringUtils.hasText(token) || !jwtUtil.isValid(token)) {
|
||||
throw new BizException("인증 정보가 없습니다.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
return CurrentUser.builder()
|
||||
.usrId(jwtUtil.getUserId(token))
|
||||
.corpNo(jwtUtil.getCorpNo(token))
|
||||
.roleCode(jwtUtil.getRoleCode(token))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static String getUsrId() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || !auth.isAuthenticated()) {
|
||||
throw new BizException("인증 정보가 없습니다.", HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
return auth.getName();
|
||||
}
|
||||
|
||||
private static String resolveToken() {
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null) return null;
|
||||
HttpServletRequest request = attrs.getRequest();
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.company.gw.envset.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.envset.dto.CodeDto;
|
||||
import com.company.gw.envset.dto.CodeIndexDto;
|
||||
import com.company.gw.envset.service.CodeManageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 환경설정 > 공통코드 관리 (Envset0020/0030)
|
||||
* GET /api/envset/codes 코드인덱스 목록
|
||||
* POST /api/envset/codes 코드인덱스 배치 저장
|
||||
* GET /api/envset/codes/{commClCd} 코드 목록
|
||||
* POST /api/envset/codes/{commClCd} 코드 배치 저장
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/envset/codes")
|
||||
@RequiredArgsConstructor
|
||||
public class CodeManageController {
|
||||
|
||||
private final CodeManageService codeManageService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<CodeIndexDto>>> getCdidxList(
|
||||
@RequestParam(required = false) String searchText) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(codeManageService.getCdidxList(cu, searchText)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Void>> saveCdidxList(@RequestBody List<CodeIndexDto> list) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
codeManageService.saveCdidxList(cu, list);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@GetMapping("/{commClCd}")
|
||||
public ResponseEntity<ApiResponse<List<CodeDto>>> getCodeList(
|
||||
@PathVariable String commClCd,
|
||||
@RequestParam(required = false) String searchText) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(
|
||||
codeManageService.getCodeList(cu, commClCd, searchText)));
|
||||
}
|
||||
|
||||
@PostMapping("/{commClCd}")
|
||||
public ResponseEntity<ApiResponse<Void>> saveCodeList(
|
||||
@PathVariable String commClCd,
|
||||
@RequestBody List<CodeDto> list) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
codeManageService.saveCodeList(cu, commClCd, list);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.company.gw.envset.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.envset.dto.AuthMenuDto;
|
||||
import com.company.gw.envset.dto.MenuManageDto;
|
||||
import com.company.gw.envset.dto.UserAuthDto;
|
||||
import com.company.gw.envset.service.MenuManageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환경설정 > 메뉴관리 / 사용자권한 (Envset0050)
|
||||
*
|
||||
* GET /api/envset/menus 메뉴 목록
|
||||
* POST /api/envset/menus 메뉴 배치 저장
|
||||
* GET /api/envset/menus/auth?menuAuthCd= 권한별 메뉴 목록
|
||||
* POST /api/envset/menus/auth?menuAuthCd= 권한-메뉴 배치 저장
|
||||
* GET /api/envset/menus/user-auth 사용자-권한 목록
|
||||
* POST /api/envset/menus/user-auth 사용자-권한 배치 저장
|
||||
* GET /api/envset/menus/search-user 사용자 검색 (autocomplete)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/envset/menus")
|
||||
@RequiredArgsConstructor
|
||||
public class MenuManageController {
|
||||
|
||||
private final MenuManageService menuManageService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<MenuManageDto>>> getMenuList() {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(menuManageService.getMenuList(cu)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Void>> saveMenuList(@RequestBody List<MenuManageDto> list) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
menuManageService.saveMenuList(cu, list);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@GetMapping("/auth")
|
||||
public ResponseEntity<ApiResponse<List<AuthMenuDto>>> getAuthMenuList(
|
||||
@RequestParam String menuAuthCd) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(
|
||||
menuManageService.getAuthMenuList(cu, menuAuthCd)));
|
||||
}
|
||||
|
||||
@PostMapping("/auth")
|
||||
public ResponseEntity<ApiResponse<Void>> saveAuthMenuList(
|
||||
@RequestParam String menuAuthCd,
|
||||
@RequestBody List<AuthMenuDto> list) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
menuManageService.saveAuthMenuList(cu, menuAuthCd, list);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@GetMapping("/user-auth")
|
||||
public ResponseEntity<ApiResponse<List<UserAuthDto>>> getUserMauthList(
|
||||
@RequestParam(required = false) String menuAuthCd,
|
||||
@RequestParam(required = false) String usrNm) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(
|
||||
menuManageService.getUserMauthList(cu, menuAuthCd, usrNm)));
|
||||
}
|
||||
|
||||
@PostMapping("/user-auth")
|
||||
public ResponseEntity<ApiResponse<Void>> saveUserMauthList(@RequestBody List<UserAuthDto> list) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
menuManageService.saveUserMauthList(cu, list);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@GetMapping("/search-user")
|
||||
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> searchUser(
|
||||
@RequestParam(required = false) String keyword) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(menuManageService.searchUser(cu, keyword)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.company.gw.envset.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.envset.dto.UserDetailDto;
|
||||
import com.company.gw.envset.dto.UserSaveDto;
|
||||
import com.company.gw.envset.service.UserManageService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환경설정 > 직원정보 (Envset0010)
|
||||
* GET /api/envset/users 직원 목록 (페이징)
|
||||
* GET /api/envset/users/{usrId} 직원 상세
|
||||
* POST /api/envset/users 직원 등록
|
||||
* PUT /api/envset/users/{usrId} 직원 수정
|
||||
* DELETE /api/envset/users/{usrId} 직원 삭제
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/envset/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserManageController {
|
||||
|
||||
private final UserManageService userManageService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getUserList(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "100") int size,
|
||||
@RequestParam(required = false) String usrNm,
|
||||
@RequestParam(required = false) String teamCd,
|
||||
@RequestParam(required = false) String dutyCd,
|
||||
@RequestParam(required = false) String apprYn,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@RequestParam(defaultValue = "USR_ID") String userOrderBy) {
|
||||
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("page", page);
|
||||
params.put("size", size);
|
||||
params.put("usrNm", usrNm);
|
||||
params.put("teamCd", teamCd);
|
||||
params.put("dutyCd", dutyCd);
|
||||
params.put("apprYn", apprYn);
|
||||
params.put("includeRetireYn", includeRetireYn);
|
||||
params.put("userOrderBy", userOrderBy);
|
||||
|
||||
Map<String, Object> result = userManageService.getUserList(cu, params);
|
||||
long total = (long) result.get("total");
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.ok(
|
||||
result,
|
||||
ApiResponse.PaginationInfo.builder().page(page).size(size).total(total).build()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/{usrId}")
|
||||
public ResponseEntity<ApiResponse<UserDetailDto>> getUserDetail(@PathVariable String usrId) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ResponseEntity.ok(ApiResponse.ok(userManageService.getUserDetail(cu, usrId)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<Void>> insertUser(@Valid @RequestBody UserSaveDto dto) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
userManageService.insertUser(cu, dto);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@PutMapping("/{usrId}")
|
||||
public ResponseEntity<ApiResponse<Void>> updateUser(
|
||||
@PathVariable String usrId,
|
||||
@Valid @RequestBody UserSaveDto dto) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
dto.setUsrId(usrId);
|
||||
userManageService.updateUser(cu, dto);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{usrId}")
|
||||
public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable String usrId) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
userManageService.deleteUser(cu, usrId);
|
||||
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.company.gw.envset.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.util.JwtUtil;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.envset.service.WorkCdService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 환경설정 > 근무코드 관리 (Envset0040, SX_CO0070)
|
||||
* GET /api/envset/workcd 목록
|
||||
* POST /api/envset/workcd 등록
|
||||
* PUT /api/envset/workcd/{workCd} 수정
|
||||
* DELETE /api/envset/workcd/{workCd} 삭제
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/envset/workcd")
|
||||
@RequiredArgsConstructor
|
||||
public class WorkCdController {
|
||||
|
||||
private final WorkCdService workCdService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<List<Map<String, Object>>> getWorkCdList(
|
||||
@RequestParam(defaultValue = "") String searchText) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
return ApiResponse.ok(workCdService.getWorkCdList(cu.getCorpNo(), searchText));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<Void> insertWorkCd(@RequestBody Map<String, Object> body) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
workCdService.insertWorkCd(cu.getCorpNo(), cu.getUsrId(), body);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@PutMapping("/{workCd}")
|
||||
public ApiResponse<Void> updateWorkCd(
|
||||
@PathVariable String workCd,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
workCdService.updateWorkCd(cu.getCorpNo(), cu.getUsrId(), workCd, body);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{workCd}")
|
||||
public ApiResponse<Void> deleteWorkCd(@PathVariable String workCd) {
|
||||
CurrentUser cu = SecurityUtil.getCurrentUser(jwtUtil);
|
||||
workCdService.deleteWorkCd(cu.getCorpNo(), workCd);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class AuthMenuDto {
|
||||
private String corpNo;
|
||||
private String menuNo;
|
||||
private String upperMenuNo;
|
||||
private String menuNm;
|
||||
private String url;
|
||||
private String menuProp;
|
||||
private Integer menuOrdr;
|
||||
private String menuUseYn;
|
||||
private Integer lvl;
|
||||
private String menuAuthYn; // Y/N
|
||||
private String menuAuthCd;
|
||||
private String funcAuthCn;
|
||||
private String rgstrId;
|
||||
private String modId;
|
||||
private String rowStatus; // U만 존재 (Y이면 merge, N이면 delete)
|
||||
}
|
||||
18
backend/src/main/java/com/company/gw/envset/dto/CodeDto.java
Normal file
18
backend/src/main/java/com/company/gw/envset/dto/CodeDto.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class CodeDto {
|
||||
private String corpNo;
|
||||
private String commClCd;
|
||||
private String commCd;
|
||||
private String commCdNm;
|
||||
private String commCdUseYn;
|
||||
private String commCdDscrpt;
|
||||
private Integer commCdDsplyOrdr;
|
||||
private String rgstrId;
|
||||
private String modId;
|
||||
private String rowStatus; // I/U/D
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class CodeIndexDto {
|
||||
private String corpNo;
|
||||
private String commClCd;
|
||||
private String commClCdNm;
|
||||
private String commClCdUseYn;
|
||||
private String commClCdDscrpt;
|
||||
private Integer dsplyOrdr;
|
||||
private String rgstrId;
|
||||
private String modId;
|
||||
// 배치 저장용 상태 플래그
|
||||
private String rowStatus; // I/U/D
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class MenuManageDto {
|
||||
private String corpNo;
|
||||
private String menuNo;
|
||||
private String upperMenuNo;
|
||||
private String menuNm;
|
||||
private String url;
|
||||
private String rm;
|
||||
private String menuProp;
|
||||
private Integer menuOrdr;
|
||||
private String menuUseYn;
|
||||
private String controller;
|
||||
private Integer menuLvl;
|
||||
private String rgstrId;
|
||||
private String modId;
|
||||
private String rowStatus; // I/U/D
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class UserAuthDto {
|
||||
private String corpNo;
|
||||
private String usrId;
|
||||
private String usrNm;
|
||||
private String menuAuthCd;
|
||||
private String teamCd;
|
||||
private String dutyCd;
|
||||
private String retirementDate;
|
||||
private String rgstrId;
|
||||
private String rowStatus; // I/D
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class UserDetailDto {
|
||||
private String corpNo;
|
||||
private String usrId;
|
||||
private String usrSortOrdr;
|
||||
private String usrNm;
|
||||
private String loginId;
|
||||
private String dutyCd;
|
||||
private String brthDyDate;
|
||||
private String usrTelno;
|
||||
private String mtelNo;
|
||||
private String email;
|
||||
private String baseAdrs;
|
||||
private String gusoAdrs;
|
||||
private String gusoTelno;
|
||||
private String joinCpDate;
|
||||
private String teamCd;
|
||||
private String dismlDate;
|
||||
private String retirementDate;
|
||||
private String spkltArtcCn;
|
||||
private String photoAtchfileNo;
|
||||
private String spmtEmail;
|
||||
private String spmtMtelNo;
|
||||
private String apprYn;
|
||||
private String finalSchspNm;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class UserListDto {
|
||||
private String usrId;
|
||||
private String usrNm;
|
||||
private String teamCd;
|
||||
private String dutyCd;
|
||||
private String usrTelno;
|
||||
private String mtelNo;
|
||||
private String retirementDate;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.company.gw.envset.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter @Setter
|
||||
public class UserSaveDto {
|
||||
// 공통 (insert/update 구분용)
|
||||
private String corpNo;
|
||||
private String usrId;
|
||||
private String rgstrId;
|
||||
private String modId;
|
||||
|
||||
@NotBlank(message = "직원명은 필수입니다.")
|
||||
private String usrNm;
|
||||
|
||||
private String loginId;
|
||||
private String pw; // 평문 → 서비스에서 암호화
|
||||
private String usrSortOrdr;
|
||||
private String dutyCd;
|
||||
private String brthDyDate;
|
||||
private String usrTelno;
|
||||
private String mtelNo;
|
||||
private String email;
|
||||
private String baseAdrs;
|
||||
private String gusoAdrs;
|
||||
private String gusoTelno;
|
||||
private String joinCpDate;
|
||||
private String teamCd;
|
||||
private String dismlDate;
|
||||
private String retirementDate;
|
||||
private String spkltArtcCn;
|
||||
private String spmtEmail;
|
||||
private String spmtMtelNo;
|
||||
private String rrno;
|
||||
private String apprYn;
|
||||
private String finalSchspNm;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.company.gw.envset.mapper;
|
||||
|
||||
import com.company.gw.envset.dto.CodeDto;
|
||||
import com.company.gw.envset.dto.CodeIndexDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface CodeManageMapper {
|
||||
List<CodeIndexDto> getCdidxList(Map<String, Object> params);
|
||||
CodeIndexDto getCdidxInfo(Map<String, Object> params);
|
||||
void insertCdidx(CodeIndexDto dto);
|
||||
void updateCdidx(CodeIndexDto dto);
|
||||
void deleteCdidx(@Param("corpNo") String corpNo, @Param("commClCd") String commClCd);
|
||||
|
||||
List<CodeDto> getCodeList(Map<String, Object> params);
|
||||
void insertCode(CodeDto dto);
|
||||
void updateCode(CodeDto dto);
|
||||
void deleteCode(@Param("corpNo") String corpNo,
|
||||
@Param("commClCd") String commClCd,
|
||||
@Param("commCd") String commCd);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.company.gw.envset.mapper;
|
||||
|
||||
import com.company.gw.envset.dto.AuthMenuDto;
|
||||
import com.company.gw.envset.dto.MenuManageDto;
|
||||
import com.company.gw.envset.dto.UserAuthDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface MenuManageMapper {
|
||||
List<MenuManageDto> getMenuList(@Param("corpNo") String corpNo);
|
||||
void insertMenu(MenuManageDto dto);
|
||||
void updateMenu(MenuManageDto dto);
|
||||
void deleteMauthByMenu(@Param("corpNo") String corpNo, @Param("menuNo") String menuNo);
|
||||
void deleteMenu(@Param("corpNo") String corpNo, @Param("menuNo") String menuNo);
|
||||
|
||||
List<AuthMenuDto> getAuthMenuList(Map<String, Object> params);
|
||||
void mergeMenuAuth(AuthMenuDto dto);
|
||||
void deleteMenuAuth(@Param("corpNo") String corpNo,
|
||||
@Param("menuNo") String menuNo,
|
||||
@Param("menuAuthCd") String menuAuthCd);
|
||||
|
||||
List<UserAuthDto> getUserMauthList(Map<String, Object> params);
|
||||
void insertUserMauth(UserAuthDto dto);
|
||||
void deleteUserMauth(@Param("corpNo") String corpNo,
|
||||
@Param("usrId") String usrId,
|
||||
@Param("menuAuthCd") String menuAuthCd);
|
||||
|
||||
List<Map<String, Object>> searchUser(Map<String, Object> params);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.company.gw.envset.mapper;
|
||||
|
||||
import com.company.gw.envset.dto.UserDetailDto;
|
||||
import com.company.gw.envset.dto.UserListDto;
|
||||
import com.company.gw.envset.dto.UserSaveDto;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface UserManageMapper {
|
||||
List<UserListDto> getUserList(Map<String, Object> params);
|
||||
long getUserListCount(Map<String, Object> params);
|
||||
UserDetailDto getUserDetail(Map<String, Object> params);
|
||||
int getLoginIdCount(Map<String, Object> params);
|
||||
void insertUser(UserSaveDto dto);
|
||||
void updateUser(UserSaveDto dto);
|
||||
void updateUserPw(@Param("corpNo") String corpNo,
|
||||
@Param("usrId") String usrId,
|
||||
@Param("pw") String pw,
|
||||
@Param("modId") String modId);
|
||||
void deleteUser(@Param("corpNo") String corpNo, @Param("usrId") String usrId);
|
||||
void updateUserPhoto(Map<String, Object> params);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.company.gw.envset.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface WorkCdMapper {
|
||||
List<Map<String, Object>> getWorkCdList(Map<String, Object> param);
|
||||
void insertWorkCd(Map<String, Object> param);
|
||||
void updateWorkCd(Map<String, Object> param);
|
||||
void deleteWorkCd(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.company.gw.envset.service;
|
||||
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.envset.dto.CodeDto;
|
||||
import com.company.gw.envset.dto.CodeIndexDto;
|
||||
import com.company.gw.envset.mapper.CodeManageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CodeManageService {
|
||||
|
||||
private final CodeManageMapper codeManageMapper;
|
||||
|
||||
public List<CodeIndexDto> getCdidxList(CurrentUser cu, String searchText) {
|
||||
return codeManageMapper.getCdidxList(Map.of(
|
||||
"corpNo", cu.getCorpNo(),
|
||||
"searchText", searchText != null ? searchText : ""
|
||||
));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveCdidxList(CurrentUser cu, List<CodeIndexDto> list) {
|
||||
for (CodeIndexDto item : list) {
|
||||
item.setCorpNo(cu.getCorpNo());
|
||||
item.setRgstrId(cu.getUsrId());
|
||||
item.setModId(cu.getUsrId());
|
||||
|
||||
switch (item.getRowStatus()) {
|
||||
case "I" -> codeManageMapper.insertCdidx(item);
|
||||
case "U" -> codeManageMapper.updateCdidx(item);
|
||||
case "D" -> codeManageMapper.deleteCdidx(cu.getCorpNo(), item.getCommClCd());
|
||||
default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<CodeDto> getCodeList(CurrentUser cu, String commClCd, String searchText) {
|
||||
if (!StringUtils.hasText(commClCd)) throw new BizException("코드분류가 필요합니다.");
|
||||
return codeManageMapper.getCodeList(Map.of(
|
||||
"corpNo", cu.getCorpNo(),
|
||||
"commClCd", commClCd,
|
||||
"searchText", searchText != null ? searchText : ""
|
||||
));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveCodeList(CurrentUser cu, String commClCd, List<CodeDto> list) {
|
||||
if (!StringUtils.hasText(commClCd)) throw new BizException("코드분류가 필요합니다.");
|
||||
for (CodeDto item : list) {
|
||||
item.setCorpNo(cu.getCorpNo());
|
||||
item.setCommClCd(commClCd);
|
||||
item.setRgstrId(cu.getUsrId());
|
||||
item.setModId(cu.getUsrId());
|
||||
|
||||
switch (item.getRowStatus()) {
|
||||
case "I" -> codeManageMapper.insertCode(item);
|
||||
case "U" -> codeManageMapper.updateCode(item);
|
||||
case "D" -> codeManageMapper.deleteCode(cu.getCorpNo(), commClCd, item.getCommCd());
|
||||
default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.company.gw.envset.service;
|
||||
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.envset.dto.AuthMenuDto;
|
||||
import com.company.gw.envset.dto.MenuManageDto;
|
||||
import com.company.gw.envset.dto.UserAuthDto;
|
||||
import com.company.gw.envset.mapper.MenuManageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MenuManageService {
|
||||
|
||||
private final MenuManageMapper menuManageMapper;
|
||||
|
||||
public List<MenuManageDto> getMenuList(CurrentUser cu) {
|
||||
return menuManageMapper.getMenuList(cu.getCorpNo());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveMenuList(CurrentUser cu, List<MenuManageDto> list) {
|
||||
for (MenuManageDto item : list) {
|
||||
item.setCorpNo(cu.getCorpNo());
|
||||
item.setRgstrId(cu.getUsrId());
|
||||
item.setModId(cu.getUsrId());
|
||||
|
||||
switch (item.getRowStatus()) {
|
||||
case "I" -> menuManageMapper.insertMenu(item);
|
||||
case "U" -> menuManageMapper.updateMenu(item);
|
||||
case "D" -> {
|
||||
menuManageMapper.deleteMauthByMenu(cu.getCorpNo(), item.getMenuNo());
|
||||
menuManageMapper.deleteMenu(cu.getCorpNo(), item.getMenuNo());
|
||||
}
|
||||
default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<AuthMenuDto> getAuthMenuList(CurrentUser cu, String menuAuthCd) {
|
||||
if (!StringUtils.hasText(menuAuthCd)) throw new BizException("권한코드가 필요합니다.");
|
||||
return menuManageMapper.getAuthMenuList(Map.of(
|
||||
"corpNo", cu.getCorpNo(),
|
||||
"menuAuthCd", menuAuthCd
|
||||
));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveAuthMenuList(CurrentUser cu, String menuAuthCd, List<AuthMenuDto> list) {
|
||||
if (!StringUtils.hasText(menuAuthCd)) throw new BizException("권한코드가 필요합니다.");
|
||||
for (AuthMenuDto item : list) {
|
||||
if (!"U".equals(item.getRowStatus())) continue;
|
||||
item.setCorpNo(cu.getCorpNo());
|
||||
item.setMenuAuthCd(menuAuthCd);
|
||||
item.setRgstrId(cu.getUsrId());
|
||||
item.setModId(cu.getUsrId());
|
||||
|
||||
if ("Y".equals(item.getMenuAuthYn())) {
|
||||
menuManageMapper.mergeMenuAuth(item);
|
||||
} else {
|
||||
menuManageMapper.deleteMenuAuth(cu.getCorpNo(), item.getMenuNo(), menuAuthCd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<UserAuthDto> getUserMauthList(CurrentUser cu, String menuAuthCd, String usrNm) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
params.put("menuAuthCd", StringUtils.hasText(menuAuthCd) ? menuAuthCd : null);
|
||||
params.put("usrNm", StringUtils.hasText(usrNm) ? usrNm : null);
|
||||
return menuManageMapper.getUserMauthList(params);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveUserMauthList(CurrentUser cu, List<UserAuthDto> list) {
|
||||
for (UserAuthDto item : list) {
|
||||
item.setCorpNo(cu.getCorpNo());
|
||||
item.setRgstrId(cu.getUsrId());
|
||||
|
||||
switch (item.getRowStatus()) {
|
||||
case "I" -> menuManageMapper.insertUserMauth(item);
|
||||
case "D" -> menuManageMapper.deleteUserMauth(
|
||||
cu.getCorpNo(), item.getUsrId(), item.getMenuAuthCd());
|
||||
default -> throw new BizException("잘못된 rowStatus: " + item.getRowStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> searchUser(CurrentUser cu, String keyword) {
|
||||
return menuManageMapper.searchUser(Map.of(
|
||||
"corpNo", cu.getCorpNo(),
|
||||
"keyword", keyword != null ? keyword : ""
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.company.gw.envset.service;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.service.SequenceService;
|
||||
import com.company.gw.common.util.PasswordUtil;
|
||||
import com.company.gw.envset.dto.UserDetailDto;
|
||||
import com.company.gw.envset.dto.UserListDto;
|
||||
import com.company.gw.envset.dto.UserSaveDto;
|
||||
import com.company.gw.envset.mapper.UserManageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserManageService {
|
||||
|
||||
private static final List<String> ALLOWED_ORDER_BY =
|
||||
List.of("USR_ID", "USR_NM", "TEAM_CD", "DUTY_CD");
|
||||
|
||||
private final UserManageMapper userManageMapper;
|
||||
private final SequenceService sequenceService;
|
||||
|
||||
public Map<String, Object> getUserList(CurrentUser cu, Map<String, Object> params) {
|
||||
String orderBy = (String) params.getOrDefault("userOrderBy", "USR_ID");
|
||||
if (!ALLOWED_ORDER_BY.contains(orderBy)) {
|
||||
throw new BizException("정렬 조건이 올바르지 않습니다.");
|
||||
}
|
||||
params.put("corpNo", cu.getCorpNo());
|
||||
params.put("userOrderBy", orderBy);
|
||||
|
||||
long total = userManageMapper.getUserListCount(params);
|
||||
List<UserListDto> list = userManageMapper.getUserList(params);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
public UserDetailDto getUserDetail(CurrentUser cu, String usrId) {
|
||||
Map<String, Object> params = Map.of("corpNo", cu.getCorpNo(), "usrId", usrId);
|
||||
UserDetailDto detail = userManageMapper.getUserDetail(params);
|
||||
if (detail == null) {
|
||||
throw new BizException("사용자 정보가 없습니다.");
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insertUser(CurrentUser cu, UserSaveDto dto) {
|
||||
if (!StringUtils.hasText(dto.getPw())) {
|
||||
throw new BizException("비밀번호는 필수입니다.");
|
||||
}
|
||||
dto.setCorpNo(cu.getCorpNo());
|
||||
dto.setRgstrId(cu.getUsrId());
|
||||
|
||||
// 시퀀스로 USR_ID 생성 (기존 getNextSeqString('SX_GW0010.USR_ID', 5))
|
||||
String usrId = sequenceService.getNextSeqString("SX_GW0010.USR_ID", 5);
|
||||
dto.setUsrId(usrId);
|
||||
|
||||
// 로그인ID 중복 체크
|
||||
checkLoginIdDuplicate(cu.getCorpNo(), dto.getLoginId(), usrId);
|
||||
|
||||
// 비밀번호 암호화 (SHA-256 + Base64)
|
||||
dto.setPw(PasswordUtil.encode(dto.getPw()));
|
||||
|
||||
userManageMapper.insertUser(dto);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateUser(CurrentUser cu, UserSaveDto dto) {
|
||||
if (!StringUtils.hasText(dto.getUsrId())) {
|
||||
throw new BizException("USR_ID는 필수입니다.");
|
||||
}
|
||||
dto.setCorpNo(cu.getCorpNo());
|
||||
dto.setModId(cu.getUsrId());
|
||||
|
||||
// 로그인ID 중복 체크 (본인 제외)
|
||||
checkLoginIdDuplicate(cu.getCorpNo(), dto.getLoginId(), dto.getUsrId());
|
||||
|
||||
userManageMapper.updateUser(dto);
|
||||
|
||||
// 비밀번호 입력 시에만 변경
|
||||
if (StringUtils.hasText(dto.getPw())) {
|
||||
userManageMapper.updateUserPw(
|
||||
cu.getCorpNo(), dto.getUsrId(),
|
||||
PasswordUtil.encode(dto.getPw()), cu.getUsrId());
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(CurrentUser cu, String usrId) {
|
||||
// 사용자 존재 확인
|
||||
getUserDetail(cu, usrId);
|
||||
userManageMapper.deleteUser(cu.getCorpNo(), usrId);
|
||||
}
|
||||
|
||||
private void checkLoginIdDuplicate(String corpNo, String loginId, String usrId) {
|
||||
if (!StringUtils.hasText(loginId)) return;
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("corpNo", corpNo);
|
||||
params.put("loginId", loginId);
|
||||
params.put("usrId", usrId != null ? usrId : "");
|
||||
int cnt = userManageMapper.getLoginIdCount(params);
|
||||
if (cnt > 0) {
|
||||
throw new BizException("이미 사용 중인 로그인 아이디입니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.company.gw.envset.service;
|
||||
|
||||
import com.company.gw.envset.mapper.WorkCdMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WorkCdService {
|
||||
|
||||
private final WorkCdMapper workCdMapper;
|
||||
|
||||
public List<Map<String, Object>> getWorkCdList(String corpNo, String searchText) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("searchText", searchText);
|
||||
return workCdMapper.getWorkCdList(param);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insertWorkCd(String corpNo, String regUsrId, Map<String, Object> data) {
|
||||
data.put("corpNo", corpNo);
|
||||
data.put("regUsrId", regUsrId);
|
||||
workCdMapper.insertWorkCd(data);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateWorkCd(String corpNo, String regUsrId, String workCd, Map<String, Object> data) {
|
||||
data.put("corpNo", corpNo);
|
||||
data.put("regUsrId", regUsrId);
|
||||
data.put("workCd", workCd);
|
||||
workCdMapper.updateWorkCd(data);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteWorkCd(String corpNo, String workCd) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("workCd", workCd);
|
||||
workCdMapper.deleteWorkCd(param);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.company.gw.fedex.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.fedex.service.FedexService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/fedex")
|
||||
@RequiredArgsConstructor
|
||||
public class FedexController {
|
||||
|
||||
private final FedexService fedexService;
|
||||
|
||||
/** 목록 */
|
||||
@GetMapping("/0010")
|
||||
public ApiResponse<Map<String, Object>> getFedexList(
|
||||
@RequestParam(defaultValue = "") String searchText,
|
||||
@RequestParam(defaultValue = "1") int pageNo,
|
||||
@RequestParam(defaultValue = "100") int pageSize) {
|
||||
return ApiResponse.ok(fedexService.getFedexList(searchText, pageNo, pageSize));
|
||||
}
|
||||
|
||||
/** 상세 */
|
||||
@GetMapping("/0010/{sq}")
|
||||
public ApiResponse<Map<String, Object>> getFedexDetail(@PathVariable long sq) {
|
||||
return ApiResponse.ok(fedexService.getFedexDetail(sq));
|
||||
}
|
||||
|
||||
/** 등록 */
|
||||
@PostMapping("/0010")
|
||||
public ApiResponse<Long> insertFedex(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser cu) {
|
||||
body.put("regUserId", cu.getUsrId());
|
||||
long sq = fedexService.insertFedex(body);
|
||||
return ApiResponse.ok(sq);
|
||||
}
|
||||
|
||||
/** 첨부파일 번호 업데이트 */
|
||||
@PatchMapping("/0010/{sq}/attach")
|
||||
public ApiResponse<Void> updateFedexAttach(
|
||||
@PathVariable long sq,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
fedexService.updateFedexAttach(sq, (String) body.get("attachNo"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 코드 목록 */
|
||||
@GetMapping("/codes")
|
||||
public ApiResponse<Map<String, Object>> getCodes() {
|
||||
return ApiResponse.ok(fedexService.getCodes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.company.gw.fedex.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface FedexMapper {
|
||||
int countFedexList(Map<String, Object> param);
|
||||
List<Map<String, Object>> getFedexList(Map<String, Object> param);
|
||||
Map<String, Object> getFedexDetail(Map<String, Object> param);
|
||||
void insertFedex(Map<String, Object> param);
|
||||
void updateFedexAttach(Map<String, Object> param);
|
||||
List<Map<String, Object>> getFstList();
|
||||
List<Map<String, Object>> getGwiList();
|
||||
List<Map<String, Object>> getFjjList();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.company.gw.fedex.service;
|
||||
|
||||
import com.company.gw.fedex.mapper.FedexMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FedexService {
|
||||
|
||||
private final FedexMapper fedexMapper;
|
||||
|
||||
public Map<String, Object> getFedexList(String searchText, int pageNo, int pageSize) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("searchText", searchText);
|
||||
param.put("pageSize", pageSize);
|
||||
param.put("offset", (pageNo - 1) * pageSize);
|
||||
|
||||
int total = fedexMapper.countFedexList(param);
|
||||
List<Map<String, Object>> list = fedexMapper.getFedexList(param);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> getFedexDetail(long sq) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("sq", sq);
|
||||
return fedexMapper.getFedexDetail(param);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public long insertFedex(Map<String, Object> param) {
|
||||
fedexMapper.insertFedex(param);
|
||||
return ((Number) param.get("sq")).longValue();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateFedexAttach(long sq, String attachNo) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("sq", sq);
|
||||
param.put("attachNo", attachNo);
|
||||
fedexMapper.updateFedexAttach(param);
|
||||
}
|
||||
|
||||
public Map<String, Object> getCodes() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("fstList", fedexMapper.getFstList());
|
||||
result.put("gwiList", fedexMapper.getGwiList());
|
||||
result.put("fjjList", fedexMapper.getFjjList());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.company.gw.main.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.main.service.MainService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/main")
|
||||
@RequiredArgsConstructor
|
||||
public class MainController {
|
||||
|
||||
private final MainService mainService;
|
||||
|
||||
/** 대시보드 전체 데이터 */
|
||||
@GetMapping("/dashboard")
|
||||
public ApiResponse<Map<String, Object>> getDashboard(
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(mainService.getDashboard(
|
||||
currentUser.getCorpNo(), currentUser.getUsrId()));
|
||||
}
|
||||
|
||||
/** 출근 */
|
||||
@PostMapping("/workstart")
|
||||
public ApiResponse<Map<String, Object>> clockIn(
|
||||
@AuthenticationPrincipal CurrentUser currentUser,
|
||||
@RequestBody Map<String, String> body,
|
||||
HttpServletRequest request) {
|
||||
String date = body.get("workPlanYymmdd");
|
||||
String ip = getClientIp(request);
|
||||
return ApiResponse.ok(mainService.clockIn(
|
||||
currentUser.getCorpNo(), currentUser.getUsrId(), date, ip));
|
||||
}
|
||||
|
||||
/** 퇴근 */
|
||||
@PostMapping("/workend")
|
||||
public ApiResponse<Map<String, Object>> clockOut(
|
||||
@AuthenticationPrincipal CurrentUser currentUser,
|
||||
@RequestBody Map<String, String> body,
|
||||
HttpServletRequest request) {
|
||||
String date = body.get("workPlanYymmdd");
|
||||
String ip = getClientIp(request);
|
||||
return ApiResponse.ok(mainService.clockOut(
|
||||
currentUser.getCorpNo(), currentUser.getUsrId(), date, ip));
|
||||
}
|
||||
|
||||
/** 오늘 지각자 목록 (관리자용) */
|
||||
@GetMapping("/late-list")
|
||||
public ApiResponse<List<Map<String, Object>>> getLateList(
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(mainService.getTodayLateList(currentUser.getCorpNo()));
|
||||
}
|
||||
|
||||
/** X-Forwarded-For → remoteAddr 순으로 클라이언트 IP 추출 */
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String xff = request.getHeader("X-Forwarded-For");
|
||||
if (StringUtils.hasText(xff)) {
|
||||
return xff.split(",")[0].trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.company.gw.main.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface MainMapper {
|
||||
List<Map<String, Object>> getTopPostList(Map<String, Object> param);
|
||||
List<Map<String, Object>> getWorkRangeList(Map<String, Object> param);
|
||||
Map<String, Object> getTodayWorkRecord(Map<String, Object> param);
|
||||
Map<String, Object> getPendingApprCount(Map<String, Object> param);
|
||||
List<Map<String, Object>> getMyDocSentSummary(Map<String, Object> param);
|
||||
List<Map<String, Object>> getMyDocReceivedSummary(Map<String, Object> param);
|
||||
List<Map<String, Object>> getTodayWorkSummary(Map<String, Object> param);
|
||||
|
||||
// 출퇴근
|
||||
Map<String, Object> getWorkplanInfo(Map<String, Object> param);
|
||||
List<String> getAllowedIpList(Map<String, Object> param);
|
||||
int updateWorkStart(Map<String, Object> param);
|
||||
int updateWorkEnd(Map<String, Object> param);
|
||||
List<Map<String, Object>> getTodayLateList(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.company.gw.main.service;
|
||||
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.main.mapper.MainMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MainService {
|
||||
|
||||
private final MainMapper mainMapper;
|
||||
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
/** 대시보드 전체 데이터 */
|
||||
public Map<String, Object> getDashboard(String corpNo, String usrId) {
|
||||
LocalDate today = LocalDate.now();
|
||||
String todayStr = today.format(FMT);
|
||||
String fromDate = today.minusDays(4).format(FMT);
|
||||
String toDate = today.plusDays(4).format(FMT);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 공지사항 최신 5건 (UNTY_BBS_CD='0002')
|
||||
result.put("noticeList", getTopPostList(corpNo, "0002", 5));
|
||||
// 자유게시판 최신 5건 (UNTY_BBS_CD='0001')
|
||||
result.put("boardList", getTopPostList(corpNo, "0001", 5));
|
||||
// 업무메뉴얼 최신 5건 (UNTY_BBS_CD='0004')
|
||||
result.put("manualList", getTopPostList(corpNo, "0004", 5));
|
||||
|
||||
// 9일 근무 범위 (오늘±4일)
|
||||
Map<String, Object> wp = new HashMap<>();
|
||||
wp.put("corpNo", corpNo);
|
||||
wp.put("usrId", usrId);
|
||||
wp.put("fromDate", fromDate);
|
||||
wp.put("toDate", toDate);
|
||||
result.put("workRangeList", mainMapper.getWorkRangeList(wp));
|
||||
|
||||
// 오늘 근무 기록
|
||||
result.put("todayWork", getTodayWorkRecord(corpNo, usrId, todayStr));
|
||||
|
||||
// 결재 대기 건수
|
||||
result.put("pendingAppr", getPendingApprCount(corpNo, usrId));
|
||||
|
||||
// 내가 올린 결재 문서 상태별 카운트
|
||||
Map<String, Object> sp = new HashMap<>();
|
||||
sp.put("corpNo", corpNo);
|
||||
sp.put("usrId", usrId);
|
||||
result.put("myDocSent", mainMapper.getMyDocSentSummary(sp));
|
||||
|
||||
// 내가 받은 결재 문서 상태별 카운트
|
||||
result.put("myDocReceived", mainMapper.getMyDocReceivedSummary(sp));
|
||||
|
||||
// 오늘 전체 근무 현황 (근무코드별 그룹)
|
||||
Map<String, Object> ts = new HashMap<>();
|
||||
ts.put("corpNo", corpNo);
|
||||
ts.put("today", todayStr);
|
||||
result.put("todayWorkSummary", mainMapper.getTodayWorkSummary(ts));
|
||||
|
||||
// 오늘 지각자
|
||||
result.put("todayLateList", getTodayLateList(corpNo));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> getTopPostList(String corpNo, String bbsCd, int top) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("bbsCd", bbsCd);
|
||||
param.put("top", top);
|
||||
return mainMapper.getTopPostList(param);
|
||||
}
|
||||
|
||||
private Map<String, Object> getTodayWorkRecord(String corpNo, String usrId, String today) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
param.put("today", today);
|
||||
return mainMapper.getTodayWorkRecord(param);
|
||||
}
|
||||
|
||||
private Map<String, Object> getPendingApprCount(String corpNo, String usrId) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
return mainMapper.getPendingApprCount(param);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// 출퇴근
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/** 근무계획 단건 조회 */
|
||||
public Map<String, Object> getWorkplanInfo(String corpNo, String usrId, String workPlanYymmdd) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
param.put("workPlanYymmdd", workPlanYymmdd);
|
||||
return mainMapper.getWorkplanInfo(param);
|
||||
}
|
||||
|
||||
/** IP 허용 여부 검사 (SX014 코드에 IP 등록 없으면 전체 허용) */
|
||||
public void checkAllowedIp(String corpNo, String clientIp) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
List<String> ipList = mainMapper.getAllowedIpList(param);
|
||||
if (ipList == null || ipList.isEmpty()) return; // 설정 없으면 전체 허용
|
||||
for (String allowed : ipList) {
|
||||
if (StringUtils.hasText(allowed) && clientIp.startsWith(allowed.trim())) return;
|
||||
}
|
||||
throw new BizException("접속 IP(" + clientIp + ")가 허용 IP가 아닙니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
/** 출근 처리 */
|
||||
@Transactional
|
||||
public Map<String, Object> clockIn(String corpNo, String usrId, String workPlanYymmdd, String clientIp) {
|
||||
checkAllowedIp(corpNo, clientIp);
|
||||
|
||||
Map<String, Object> wp = getWorkplanInfo(corpNo, usrId, workPlanYymmdd);
|
||||
if (wp == null || !StringUtils.hasText((String) wp.get("REAL_WORK_CD"))) {
|
||||
throw new BizException(workPlanYymmdd + " 일의 근무 계획이 없습니다.");
|
||||
}
|
||||
if (StringUtils.hasText((String) wp.get("WORK_START_DT"))) {
|
||||
throw new BizException(workPlanYymmdd + " 에 이미 출근 기록이 있습니다.");
|
||||
}
|
||||
|
||||
// 지각 여부 계산
|
||||
int lateMin = calcLateMin((String) wp.get("GOTOWORK_TM_NM"));
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
param.put("workPlanYymmdd", workPlanYymmdd);
|
||||
mainMapper.updateWorkStart(param);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("lateMin", lateMin);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 퇴근 처리 */
|
||||
@Transactional
|
||||
public Map<String, Object> clockOut(String corpNo, String usrId, String workPlanYymmdd, String clientIp) {
|
||||
checkAllowedIp(corpNo, clientIp);
|
||||
|
||||
Map<String, Object> wp = getWorkplanInfo(corpNo, usrId, workPlanYymmdd);
|
||||
if (wp == null || !StringUtils.hasText((String) wp.get("REAL_WORK_CD"))) {
|
||||
throw new BizException(workPlanYymmdd + " 일의 근무 계획이 없습니다.");
|
||||
}
|
||||
if (!StringUtils.hasText((String) wp.get("WORK_START_DT"))) {
|
||||
throw new BizException(workPlanYymmdd + " 에 출근 기록이 없습니다.");
|
||||
}
|
||||
if (StringUtils.hasText((String) wp.get("WORK_END_DT"))) {
|
||||
throw new BizException(workPlanYymmdd + " 에 이미 퇴근 기록이 있습니다.");
|
||||
}
|
||||
|
||||
// 조퇴 여부 계산
|
||||
int earlyMin = calcEarlyMin((String) wp.get("GETOFFWORK_TM_NM"));
|
||||
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("usrId", usrId);
|
||||
param.put("workPlanYymmdd", workPlanYymmdd);
|
||||
mainMapper.updateWorkEnd(param);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("earlyMin", earlyMin);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 오늘 지각자 목록 */
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getTodayLateList(String corpNo) {
|
||||
String today = LocalDate.now().format(FMT);
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("corpNo", corpNo);
|
||||
param.put("today", today);
|
||||
return mainMapper.getTodayLateList(param);
|
||||
}
|
||||
|
||||
/** 지각 분 계산 (GOTOWORK_TM_NM 예: "0900") */
|
||||
private int calcLateMin(String gotoworkTmNm) {
|
||||
if (!StringUtils.hasText(gotoworkTmNm) || gotoworkTmNm.length() < 4) return 0;
|
||||
try {
|
||||
LocalTime scheduled = LocalTime.of(
|
||||
Integer.parseInt(gotoworkTmNm.substring(0, 2)),
|
||||
Integer.parseInt(gotoworkTmNm.substring(2, 4))
|
||||
);
|
||||
LocalTime now = LocalTime.now();
|
||||
if (now.isAfter(scheduled)) {
|
||||
return (int) java.time.Duration.between(scheduled, now).toMinutes();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 조퇴 분 계산 (GETOFFWORK_TM_NM 예: "1800") */
|
||||
private int calcEarlyMin(String getoffworkTmNm) {
|
||||
if (!StringUtils.hasText(getoffworkTmNm) || getoffworkTmNm.length() < 4) return 0;
|
||||
try {
|
||||
LocalTime scheduled = LocalTime.of(
|
||||
Integer.parseInt(getoffworkTmNm.substring(0, 2)),
|
||||
Integer.parseInt(getoffworkTmNm.substring(2, 4))
|
||||
);
|
||||
LocalTime now = LocalTime.now();
|
||||
if (now.isBefore(scheduled)) {
|
||||
return (int) java.time.Duration.between(now, scheduled).toMinutes();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.company.gw.tam.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.tam.service.TamService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/tam")
|
||||
@RequiredArgsConstructor
|
||||
public class TamController {
|
||||
|
||||
private final TamService tamService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0010 - 연차 관리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/yyvct")
|
||||
public ApiResponse<List<Map<String, Object>>> getYyvctList(
|
||||
@RequestParam String yyvctYy,
|
||||
@RequestParam(defaultValue = "") String usrNm,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getYyvctList(
|
||||
currentUser.getCorpNo(), yyvctYy, usrNm, teamCd, includeRetireYn));
|
||||
}
|
||||
|
||||
@PostMapping("/yyvct")
|
||||
public ApiResponse<Void> saveYyvct(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.saveYyvct(
|
||||
currentUser.getCorpNo(),
|
||||
(String) body.get("yyvctYy"),
|
||||
(String) body.get("usrId"),
|
||||
Integer.parseInt(body.get("yyvctCnt").toString()));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/yyvct")
|
||||
public ApiResponse<Void> deleteYyvct(
|
||||
@RequestParam String yyvctYy,
|
||||
@RequestParam String usrId,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.deleteYyvct(currentUser.getCorpNo(), yyvctYy, usrId);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0020 - 결재 신청
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/apvreq")
|
||||
public ApiResponse<Map<String, Object>> getApvreqList(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getApvreqList(currentUser.getCorpNo(), params));
|
||||
}
|
||||
|
||||
@GetMapping("/apvreq/{aprvlDocId}")
|
||||
public ApiResponse<Map<String, Object>> getApvreqDetail(
|
||||
@PathVariable String aprvlDocId,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getApvreqDetail(currentUser.getCorpNo(), aprvlDocId));
|
||||
}
|
||||
|
||||
@PostMapping("/apvreq")
|
||||
public ApiResponse<Map<String, String>> createApvreq(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
String aprvlDocId = tamService.createApvreq(currentUser.getCorpNo(), body);
|
||||
return ApiResponse.ok(Map.of("aprvlDocId", aprvlDocId));
|
||||
}
|
||||
|
||||
@PutMapping("/apvreq/{aprvlDocId}")
|
||||
public ApiResponse<Void> updateApvreq(
|
||||
@PathVariable String aprvlDocId,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.updateApvreq(currentUser.getCorpNo(), aprvlDocId, body);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@PostMapping("/apvreq/{aprvlDocId}/request")
|
||||
public ApiResponse<Void> requestApvreq(
|
||||
@PathVariable String aprvlDocId,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> apprList = (List<Map<String, Object>>) body.get("apprList");
|
||||
tamService.requestApvreq(currentUser.getCorpNo(), aprvlDocId, apprList);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/apvreq/{aprvlDocId}")
|
||||
public ApiResponse<Void> deleteApvreq(
|
||||
@PathVariable String aprvlDocId,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.deleteApvreq(currentUser.getCorpNo(), aprvlDocId);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@GetMapping("/apvreq/latest-appr")
|
||||
public ApiResponse<List<Map<String, Object>>> getLatestApprList(
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getLatestApprList(currentUser.getCorpNo()));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0030 - 결재 처리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/apvapp")
|
||||
public ApiResponse<Map<String, Object>> getApvappList(
|
||||
@RequestParam Map<String, Object> params,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getApvappList(currentUser.getCorpNo(), params));
|
||||
}
|
||||
|
||||
@GetMapping("/apvapp/{aprvlDocId}")
|
||||
public ApiResponse<Map<String, Object>> getApvappDetail(
|
||||
@PathVariable String aprvlDocId,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getApvappDetail(currentUser.getCorpNo(), aprvlDocId));
|
||||
}
|
||||
|
||||
@PostMapping("/apvapp/{aprvlDocId}/approve")
|
||||
public ApiResponse<Void> approve(
|
||||
@PathVariable String aprvlDocId,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.approveApvdoc(currentUser.getCorpNo(), aprvlDocId,
|
||||
Integer.parseInt(body.get("aprvlSno").toString()),
|
||||
(String) body.get("apprCn"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@PostMapping("/apvapp/{aprvlDocId}/reject")
|
||||
public ApiResponse<Void> reject(
|
||||
@PathVariable String aprvlDocId,
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
tamService.rejectApvdoc(currentUser.getCorpNo(), aprvlDocId,
|
||||
Integer.parseInt(body.get("aprvlSno").toString()),
|
||||
(String) body.get("apprCn"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 일괄 승인 */
|
||||
@PostMapping("/apvapp/multi-approve")
|
||||
public ApiResponse<Void> multiApprove(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) body.get("items");
|
||||
tamService.multiApprove(currentUser.getCorpNo(), items, (String) body.get("apprCn"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
/** 일괄 반려 */
|
||||
@PostMapping("/apvapp/multi-reject")
|
||||
public ApiResponse<Void> multiReject(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) body.get("items");
|
||||
tamService.multiReject(currentUser.getCorpNo(), items, (String) body.get("apprCn"));
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0040 - 근태 현황
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/status")
|
||||
public ApiResponse<List<Map<String, Object>>> getTamStatusList(
|
||||
@RequestParam String yyvctYy,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "00000000") String staYmd,
|
||||
@RequestParam(defaultValue = "99999999") String endYmd,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(tamService.getTamStatusList(
|
||||
currentUser.getCorpNo(), yyvctYy, teamCd, staYmd, endYmd, includeRetireYn));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.company.gw.tam.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface ApvdocMapper {
|
||||
|
||||
Map<String, Object> getApvdocInfo(Map<String, Object> param);
|
||||
|
||||
List<Map<String, Object>> getApprList(Map<String, Object> param);
|
||||
|
||||
Map<String, Object> getApprInfo(Map<String, Object> param);
|
||||
|
||||
void insertApvdoc(Map<String, Object> param);
|
||||
|
||||
void updateApvdocContent(Map<String, Object> param);
|
||||
|
||||
void updateApvdocAtchNo(Map<String, Object> param);
|
||||
|
||||
void updateApvdocStatusRequest(Map<String, Object> param);
|
||||
|
||||
void updateApvdocStatusApprove(Map<String, Object> param);
|
||||
|
||||
void updateApvdocStatusReject(Map<String, Object> param);
|
||||
|
||||
void updateFinalAprvlSno(Map<String, Object> param);
|
||||
|
||||
void updateAprvlCmplSno(Map<String, Object> param);
|
||||
|
||||
void deleteApprAll(Map<String, Object> param);
|
||||
|
||||
void insertAppr(Map<String, Object> param);
|
||||
|
||||
void updateApprStatus(Map<String, Object> param);
|
||||
|
||||
void apprApproveOrReject(Map<String, Object> param);
|
||||
|
||||
void updateNextApprStatusAppring(Map<String, Object> param);
|
||||
|
||||
void deleteApvdocSx0080(Map<String, Object> param);
|
||||
|
||||
void deleteApvdocSx0110(Map<String, Object> param);
|
||||
|
||||
void deleteApvdocSx0090(Map<String, Object> param);
|
||||
|
||||
void updateApvdocDocFlagAplnt(Map<String, Object> param);
|
||||
|
||||
void resetWorkEndDt(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.company.gw.tam.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface TamMapper {
|
||||
|
||||
// TAM0010 - 연차
|
||||
List<Map<String, Object>> getYyvctList(Map<String, Object> param);
|
||||
void upsertYyvct(Map<String, Object> param);
|
||||
void deleteYyvct(Map<String, Object> param);
|
||||
|
||||
// TAM0020 - 결재 신청
|
||||
List<Map<String, Object>> getApvreqList(Map<String, Object> param);
|
||||
long getApvreqListCount(Map<String, Object> param);
|
||||
List<Map<String, Object>> getWorkChangeList(Map<String, Object> param);
|
||||
Map<String, Object> getOtInfo(Map<String, Object> param);
|
||||
void insertSxGw0110(Map<String, Object> param);
|
||||
void insertSxGw0080(Map<String, Object> param);
|
||||
void deleteSxGw0110(Map<String, Object> param);
|
||||
void deleteSxGw0080(Map<String, Object> param);
|
||||
List<Map<String, Object>> getLatestApprList(Map<String, Object> param);
|
||||
void updateDocFlagAplnt(Map<String, Object> param);
|
||||
|
||||
// TAM0030 - 결재 처리
|
||||
List<Map<String, Object>> getApvappList(Map<String, Object> param);
|
||||
long getApvappListCount(Map<String, Object> param);
|
||||
void updateDocFlagAppr(Map<String, Object> param);
|
||||
void updateOutingTime(Map<String, Object> param);
|
||||
|
||||
// TAM0040 - 현황/통계
|
||||
List<Map<String, Object>> getTamStatusList(Map<String, Object> param);
|
||||
|
||||
// 결재 후처리
|
||||
int afterProcessOt(Map<String, Object> param);
|
||||
int afterProcessLate(Map<String, Object> param);
|
||||
int afterProcessEarlyDep(Map<String, Object> param);
|
||||
int afterProcessAbsence(Map<String, Object> param);
|
||||
int afterProcessWorkChange(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package com.company.gw.tam.service;
|
||||
|
||||
import com.company.gw.common.exception.BizException;
|
||||
import com.company.gw.common.service.AttachService;
|
||||
import com.company.gw.common.service.SequenceService;
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.tam.mapper.ApvdocMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 결재 공통 서비스
|
||||
* 결재 상태코드: 0001=작성중, 0002=결재중, 0003=결재완료, 0004=반려
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ApvdocService {
|
||||
|
||||
public static final String STATUS_WRITING = "0001";
|
||||
public static final String STATUS_APPRING = "0002";
|
||||
public static final String STATUS_APPROVED = "0003";
|
||||
public static final String STATUS_REJECTED = "0004";
|
||||
|
||||
private final ApvdocMapper apvdocMapper;
|
||||
private final AttachService attachService;
|
||||
private final SequenceService sequenceService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 결재문서 조회
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> getApvdocInfo(String corpNo, String aprvlDocId) {
|
||||
Map<String, Object> info = apvdocMapper.getApvdocInfo(param(corpNo, aprvlDocId));
|
||||
if (info == null) throw new BizException("결재문서가 존재하지 않습니다.", HttpStatus.NOT_FOUND);
|
||||
return info;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getApprList(String corpNo, String aprvlDocId) {
|
||||
return apvdocMapper.getApprList(param(corpNo, aprvlDocId));
|
||||
}
|
||||
|
||||
/** 결재문서 + 결재자목록 + 첨부파일 통합 조회 */
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getApvdocInfoAll(String corpNo, String aprvlDocId) {
|
||||
Map<String, Object> info = getApvdocInfo(corpNo, aprvlDocId);
|
||||
info.put("apprList", getApprList(corpNo, aprvlDocId));
|
||||
String atchNo = (String) info.get("ATCH_NO");
|
||||
if (atchNo != null) {
|
||||
info.put("attachFileList", attachService.getFileList(atchNo));
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 결재문서 생성
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public String createApvdoc(String corpNo, String aprvlKindCd,
|
||||
String aplntCn, String bzCn, String offerCn, String bzDeputyId,
|
||||
String atchNo) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
String aprvlDocId = sequenceService.getNextSeqString("SX_GW0090.APRVL_DOC_ID", 10);
|
||||
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
p.put("aprvlKindCd", aprvlKindCd);
|
||||
p.put("aplntCn", aplntCn);
|
||||
p.put("bzCn", bzCn);
|
||||
p.put("offerCn", offerCn);
|
||||
p.put("bzDeputyId", bzDeputyId);
|
||||
p.put("atchNo", atchNo);
|
||||
p.put("usrId", usrId);
|
||||
|
||||
apvdocMapper.insertApvdoc(p);
|
||||
|
||||
if (org.springframework.util.StringUtils.hasText(atchNo)) {
|
||||
attachService.confirmAtchNo(atchNo, "SX_GW0090.ATCH_NO");
|
||||
}
|
||||
|
||||
return aprvlDocId;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateApvdocContent(String corpNo, String aprvlDocId,
|
||||
String aplntCn, String bzCn, String offerCn, String bzDeputyId,
|
||||
String atchNo) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> info = getApvdocInfo(corpNo, aprvlDocId);
|
||||
checkAplnt(info);
|
||||
checkBeforeRequest(info);
|
||||
|
||||
Map<String, Object> p = param(corpNo, aprvlDocId);
|
||||
p.put("aplntCn", aplntCn);
|
||||
p.put("bzCn", bzCn);
|
||||
p.put("offerCn", offerCn);
|
||||
p.put("bzDeputyId", bzDeputyId);
|
||||
p.put("usrId", usrId);
|
||||
apvdocMapper.updateApvdocContent(p);
|
||||
|
||||
if (org.springframework.util.StringUtils.hasText(atchNo)) {
|
||||
Map<String, Object> ap = param(corpNo, aprvlDocId);
|
||||
ap.put("atchNo", atchNo);
|
||||
apvdocMapper.updateApvdocAtchNo(ap);
|
||||
attachService.confirmAtchNo(atchNo, "SX_GW0090.ATCH_NO");
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 결재자 등록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void insertApprList(String corpNo, String aprvlDocId, List<Map<String, Object>> apprList) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> info = getApvdocInfo(corpNo, aprvlDocId);
|
||||
checkBeforeRequest(info);
|
||||
|
||||
apvdocMapper.deleteApprAll(param(corpNo, aprvlDocId));
|
||||
|
||||
int sno = 1;
|
||||
for (Map<String, Object> appr : apprList) {
|
||||
String apprId = (String) appr.get("apprId");
|
||||
if (apprId == null || apprId.isEmpty()) {
|
||||
throw new BizException("결재자가 지정되지 않았습니다.");
|
||||
}
|
||||
Map<String, Object> p = param(corpNo, aprvlDocId);
|
||||
p.put("aprvlSno", sno++);
|
||||
p.put("apprId", apprId);
|
||||
p.put("usrId", usrId);
|
||||
apvdocMapper.insertAppr(p);
|
||||
}
|
||||
apvdocMapper.updateFinalAprvlSno(param(corpNo, aprvlDocId));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 결재 상신
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void requestApvdoc(String corpNo, String aprvlDocId) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
Map<String, Object> info = getApvdocInfo(corpNo, aprvlDocId);
|
||||
checkBeforeRequest(info);
|
||||
|
||||
List<Map<String, Object>> apprList = getApprList(corpNo, aprvlDocId);
|
||||
if (apprList.isEmpty()) throw new BizException("결재자가 지정되지 않았습니다.");
|
||||
|
||||
// 1차 결재자 → 결재중
|
||||
Map<String, Object> p1 = param(corpNo, aprvlDocId);
|
||||
p1.put("aprvlSno", 1);
|
||||
p1.put("apprStusCd", STATUS_APPRING);
|
||||
p1.put("usrId", usrId);
|
||||
apvdocMapper.updateApprStatus(p1);
|
||||
|
||||
// 문서 → 결재중
|
||||
apvdocMapper.updateApvdocStatusRequest(param(corpNo, aprvlDocId));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 승인 / 반려
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void approveApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) {
|
||||
processApproveOrReject(corpNo, aprvlDocId, aprvlSno, apprCn, true);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void rejectApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) {
|
||||
processApproveOrReject(corpNo, aprvlDocId, aprvlSno, apprCn, false);
|
||||
}
|
||||
|
||||
private void processApproveOrReject(String corpNo, String aprvlDocId,
|
||||
int aprvlSno, String apprCn, boolean isApprove) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
|
||||
Map<String, Object> docInfo = getApvdocInfo(corpNo, aprvlDocId);
|
||||
if (!STATUS_APPRING.equals(docInfo.get("APRVL_STUS_CD"))) {
|
||||
throw new BizException("결재중 상태가 아닙니다.");
|
||||
}
|
||||
|
||||
Map<String, Object> apprParam = param(corpNo, aprvlDocId);
|
||||
apprParam.put("aprvlSno", aprvlSno);
|
||||
Map<String, Object> apprInfo = apvdocMapper.getApprInfo(apprParam);
|
||||
|
||||
if (apprInfo == null) throw new BizException("결재자 정보가 없습니다. (" + aprvlSno + "차)");
|
||||
if (!STATUS_APPRING.equals(apprInfo.get("APPR_STUS_CD"))) throw new BizException("결재중인 상태가 아닙니다.");
|
||||
if (!usrId.equals(apprInfo.get("APPR_ID"))) throw new BizException("결재자가 아닙니다.");
|
||||
|
||||
String apprStusCd = isApprove ? STATUS_APPROVED : STATUS_REJECTED;
|
||||
Map<String, Object> p = param(corpNo, aprvlDocId);
|
||||
p.put("aprvlSno", aprvlSno);
|
||||
p.put("apprStusCd", apprStusCd);
|
||||
p.put("apprCn", apprCn);
|
||||
p.put("usrId", usrId);
|
||||
apvdocMapper.apprApproveOrReject(p);
|
||||
|
||||
if (isApprove) {
|
||||
if ("Y".equals(apprInfo.get("FINAL_APPR_YN"))) {
|
||||
apvdocMapper.updateApvdocStatusApprove(param(corpNo, aprvlDocId));
|
||||
} else {
|
||||
Map<String, Object> nextP = param(corpNo, aprvlDocId);
|
||||
nextP.put("aprvlSno", aprvlSno);
|
||||
nextP.put("usrId", usrId);
|
||||
apvdocMapper.updateNextApprStatusAppring(nextP);
|
||||
}
|
||||
} else {
|
||||
apvdocMapper.updateApvdocStatusReject(param(corpNo, aprvlDocId));
|
||||
apvdocMapper.resetWorkEndDt(param(corpNo, aprvlDocId));
|
||||
}
|
||||
apvdocMapper.updateAprvlCmplSno(param(corpNo, aprvlDocId));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 결재문서 삭제
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public void deleteApvdoc(String corpNo, String aprvlDocId) {
|
||||
Map<String, Object> info = getApvdocInfo(corpNo, aprvlDocId);
|
||||
checkAplnt(info);
|
||||
Map<String, Object> p = param(corpNo, aprvlDocId);
|
||||
apvdocMapper.deleteApvdocSx0080(p);
|
||||
apvdocMapper.deleteApvdocSx0110(p);
|
||||
apvdocMapper.deleteApvdocSx0090(p);
|
||||
|
||||
String atchNo = (String) info.get("ATCH_NO");
|
||||
if (atchNo != null) attachService.deleteFilesByAtchNo(atchNo, true);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 내부 유틸
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
private Map<String, Object> param(String corpNo, String aprvlDocId) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
return p;
|
||||
}
|
||||
|
||||
private void checkBeforeRequest(Map<String, Object> docInfo) {
|
||||
if (!STATUS_WRITING.equals(docInfo.get("APRVL_STUS_CD"))) {
|
||||
throw new BizException("작성중 상태가 아닙니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAplnt(Map<String, Object> docInfo) {
|
||||
String usrId = SecurityUtil.getUsrId();
|
||||
if (!usrId.equals(docInfo.get("APLNT_ID"))) {
|
||||
throw new BizException("신청자가 아닙니다.", HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
315
backend/src/main/java/com/company/gw/tam/service/TamService.java
Normal file
315
backend/src/main/java/com/company/gw/tam/service/TamService.java
Normal file
@@ -0,0 +1,315 @@
|
||||
package com.company.gw.tam.service;
|
||||
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.tam.mapper.TamMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TamService {
|
||||
|
||||
private final TamMapper tamMapper;
|
||||
private final ApvdocService apvdocService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0010 - 연차 관리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getYyvctList(String corpNo, String yyvctYy,
|
||||
String usrNm, String teamCd,
|
||||
String includeRetireYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("yyvctYy", yyvctYy);
|
||||
p.put("usrNm", usrNm);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N");
|
||||
return tamMapper.getYyvctList(p);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void saveYyvct(String corpNo, String yyvctYy, String usrId, int yyvctCnt) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("yyvctYy", yyvctYy);
|
||||
p.put("usrId", usrId);
|
||||
p.put("yyvctCnt", yyvctCnt);
|
||||
p.put("loginUsrId", SecurityUtil.getUsrId());
|
||||
tamMapper.upsertYyvct(p);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteYyvct(String corpNo, String yyvctYy, String usrId) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("yyvctYy", yyvctYy);
|
||||
p.put("usrId", usrId);
|
||||
tamMapper.deleteYyvct(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0020 - 결재 신청 목록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getApvreqList(String corpNo, Map<String, Object> filter) {
|
||||
filter.put("corpNo", corpNo);
|
||||
filter.put("usrId", SecurityUtil.getUsrId());
|
||||
filter.put("pageNo", Integer.parseInt(filter.getOrDefault("pageNo", "1").toString()));
|
||||
filter.put("pageSize", Integer.parseInt(filter.getOrDefault("pageSize", "20").toString()));
|
||||
if (!filter.containsKey("staYmd") || filter.get("staYmd") == null || filter.get("staYmd").toString().isEmpty())
|
||||
filter.put("staYmd", "00000000");
|
||||
if (!filter.containsKey("endYmd") || filter.get("endYmd") == null || filter.get("endYmd").toString().isEmpty())
|
||||
filter.put("endYmd", "99999999");
|
||||
|
||||
List<Map<String, Object>> list = tamMapper.getApvreqList(filter);
|
||||
long total = tamMapper.getApvreqListCount(filter);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 결재 신청 상세 (문서 + 결재자 + 첨부 + 세부항목) */
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getApvreqDetail(String corpNo, String aprvlDocId) {
|
||||
Map<String, Object> info = apvdocService.getApvdocInfoAll(corpNo, aprvlDocId);
|
||||
|
||||
// 변경근무 목록
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
info.put("workChangeList", tamMapper.getWorkChangeList(p));
|
||||
info.put("otInfo", tamMapper.getOtInfo(p));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/** 결재 신청 생성 */
|
||||
@Transactional
|
||||
public String createApvreq(String corpNo, Map<String, Object> body) {
|
||||
String aprvlDocId = apvdocService.createApvdoc(
|
||||
corpNo,
|
||||
(String) body.get("aprvlKindCd"),
|
||||
(String) body.get("aplntCn"),
|
||||
(String) body.get("bzCn"),
|
||||
(String) body.get("offerCn"),
|
||||
(String) body.get("bzDeputyId"),
|
||||
(String) body.get("atchNo")
|
||||
);
|
||||
|
||||
// 세부 항목 등록
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
p.put("usrId", SecurityUtil.getUsrId());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> workChangeList = (List<Map<String, Object>>) body.get("workChangeList");
|
||||
if (workChangeList != null) {
|
||||
for (Map<String, Object> item : workChangeList) {
|
||||
Map<String, Object> ip = new HashMap<>(p);
|
||||
ip.putAll(item);
|
||||
tamMapper.insertSxGw0110(ip);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> otInfo = (Map<String, Object>) body.get("otInfo");
|
||||
if (otInfo != null) {
|
||||
Map<String, Object> ip = new HashMap<>(p);
|
||||
ip.putAll(otInfo);
|
||||
tamMapper.insertSxGw0080(ip);
|
||||
}
|
||||
|
||||
return aprvlDocId;
|
||||
}
|
||||
|
||||
/** 결재 신청 수정 */
|
||||
@Transactional
|
||||
public void updateApvreq(String corpNo, String aprvlDocId, Map<String, Object> body) {
|
||||
apvdocService.updateApvdocContent(corpNo, aprvlDocId,
|
||||
(String) body.get("aplntCn"), (String) body.get("bzCn"),
|
||||
(String) body.get("offerCn"), (String) body.get("bzDeputyId"),
|
||||
(String) body.get("atchNo"));
|
||||
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
p.put("usrId", SecurityUtil.getUsrId());
|
||||
|
||||
tamMapper.deleteSxGw0110(p);
|
||||
tamMapper.deleteSxGw0080(p);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> workChangeList = (List<Map<String, Object>>) body.get("workChangeList");
|
||||
if (workChangeList != null) {
|
||||
for (Map<String, Object> item : workChangeList) {
|
||||
Map<String, Object> ip = new HashMap<>(p);
|
||||
ip.putAll(item);
|
||||
tamMapper.insertSxGw0110(ip);
|
||||
}
|
||||
}
|
||||
Map<String, Object> otInfo = (Map<String, Object>) body.get("otInfo");
|
||||
if (otInfo != null) {
|
||||
Map<String, Object> ip = new HashMap<>(p);
|
||||
ip.putAll(otInfo);
|
||||
tamMapper.insertSxGw0080(ip);
|
||||
}
|
||||
}
|
||||
|
||||
/** 결재 상신 (결재자 등록 + 상신) */
|
||||
@Transactional
|
||||
public void requestApvreq(String corpNo, String aprvlDocId, List<Map<String, Object>> apprList) {
|
||||
apvdocService.insertApprList(corpNo, aprvlDocId, apprList);
|
||||
apvdocService.requestApvdoc(corpNo, aprvlDocId);
|
||||
}
|
||||
|
||||
/** 결재문서 삭제 */
|
||||
@Transactional
|
||||
public void deleteApvreq(String corpNo, String aprvlDocId) {
|
||||
apvdocService.deleteApvdoc(corpNo, aprvlDocId);
|
||||
}
|
||||
|
||||
/** 최근 결재자 목록 조회 */
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getLatestApprList(String corpNo) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("usrId", SecurityUtil.getUsrId());
|
||||
return tamMapper.getLatestApprList(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0030 - 결재 처리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getApvappList(String corpNo, Map<String, Object> filter) {
|
||||
filter.put("corpNo", corpNo);
|
||||
filter.put("usrId", SecurityUtil.getUsrId());
|
||||
filter.put("pageNo", Integer.parseInt(filter.getOrDefault("pageNo", "1").toString()));
|
||||
filter.put("pageSize", Integer.parseInt(filter.getOrDefault("pageSize", "20").toString()));
|
||||
|
||||
List<Map<String, Object>> list = tamMapper.getApvappList(filter);
|
||||
long total = tamMapper.getApvappListCount(filter);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", list);
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 결재 처리 상세 */
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getApvappDetail(String corpNo, String aprvlDocId) {
|
||||
return apvdocService.getApvdocInfoAll(corpNo, aprvlDocId);
|
||||
}
|
||||
|
||||
/** 승인 처리 (최종 승인 시 executeAfterProcess 호출) */
|
||||
@Transactional
|
||||
public void approveApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) {
|
||||
// 승인 전 문서 정보 (aplntId, aprvlKindCd 필요)
|
||||
Map<String, Object> docBefore = apvdocService.getApvdocInfo(corpNo, aprvlDocId);
|
||||
apvdocService.approveApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn);
|
||||
// 최종 승인 여부 확인
|
||||
Map<String, Object> docAfter = apvdocService.getApvdocInfo(corpNo, aprvlDocId);
|
||||
if (ApvdocService.STATUS_APPROVED.equals(docAfter.get("APRVL_STUS_CD"))) {
|
||||
executeAfterProcess(corpNo, aprvlDocId,
|
||||
(String) docBefore.get("APRVL_KIND_CD"),
|
||||
(String) docBefore.get("APLNT_ID"));
|
||||
}
|
||||
}
|
||||
|
||||
/** 반려 처리 */
|
||||
@Transactional
|
||||
public void rejectApvdoc(String corpNo, String aprvlDocId, int aprvlSno, String apprCn) {
|
||||
apvdocService.rejectApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn);
|
||||
}
|
||||
|
||||
/** 일괄 승인 */
|
||||
@Transactional
|
||||
public void multiApprove(String corpNo, List<Map<String, Object>> items, String apprCn) {
|
||||
for (Map<String, Object> item : items) {
|
||||
String aprvlDocId = (String) item.get("aprvlDocId");
|
||||
int aprvlSno = Integer.parseInt(item.get("aprvlSno").toString());
|
||||
try {
|
||||
approveApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn);
|
||||
} catch (Exception e) {
|
||||
// 일괄 처리 중 개별 실패는 무시하고 계속 진행
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 일괄 반려 */
|
||||
@Transactional
|
||||
public void multiReject(String corpNo, List<Map<String, Object>> items, String apprCn) {
|
||||
for (Map<String, Object> item : items) {
|
||||
String aprvlDocId = (String) item.get("aprvlDocId");
|
||||
int aprvlSno = Integer.parseInt(item.get("aprvlSno").toString());
|
||||
try {
|
||||
rejectApvdoc(corpNo, aprvlDocId, aprvlSno, apprCn);
|
||||
} catch (Exception e) {
|
||||
// 일괄 처리 중 개별 실패는 무시하고 계속 진행
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 최종 승인 후처리 - 결재 종류별 근무 데이터 업데이트 */
|
||||
private void executeAfterProcess(String corpNo, String aprvlDocId,
|
||||
String aprvlKindCd, String aplntId) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("aprvlDocId", aprvlDocId);
|
||||
p.put("aplntId", aplntId);
|
||||
|
||||
switch (aprvlKindCd != null ? aprvlKindCd : "") {
|
||||
case "0001": // 시간외 → OT_RCTN_YN='Y'
|
||||
tamMapper.afterProcessOt(p);
|
||||
break;
|
||||
case "0002": // 지각 → WORK_START_DT 보정
|
||||
tamMapper.afterProcessLate(p);
|
||||
break;
|
||||
case "0003": // 조퇴 → WORK_END_DT 보정 + EL_RCTN_YN='Y'
|
||||
tamMapper.afterProcessEarlyDep(p);
|
||||
break;
|
||||
case "0004": // 결근 → 출퇴근 모두 보정
|
||||
tamMapper.afterProcessAbsence(p);
|
||||
break;
|
||||
case "0005": // 연차 → WORK_CD 변경
|
||||
case "0006": // 근무변경 → WORK_CD 변경
|
||||
tamMapper.afterProcessWorkChange(p);
|
||||
break;
|
||||
case "0008": // 외출 → OUTING_MIN/CNT 업데이트
|
||||
tamMapper.updateOutingTime(p);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// TAM0040 - 근태 현황
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getTamStatusList(String corpNo, String yyvctYy,
|
||||
String teamCd, String staYmd,
|
||||
String endYmd, String includeRetireYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("yyvctYy", yyvctYy);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("staYmd", staYmd);
|
||||
p.put("endYmd", endYmd);
|
||||
p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N");
|
||||
return tamMapper.getTamStatusList(p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.company.gw.wplan.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.wplan.service.WplanService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/wplan")
|
||||
@RequiredArgsConstructor
|
||||
public class WplanController {
|
||||
|
||||
private final WplanService wplanService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 근무코드 목록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/workcode")
|
||||
public ApiResponse<List<Map<String, Object>>> getWorkCodeList(
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wplanService.getWorkCodeList(currentUser.getCorpNo()));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0010 - 근무계획관리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/0010")
|
||||
public ApiResponse<List<Map<String, Object>>> getWplanList(
|
||||
@RequestParam String workPlanYymm,
|
||||
@RequestParam(defaultValue = "") String searchText,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wplanService.getWplanList(
|
||||
currentUser.getCorpNo(), workPlanYymm, searchText, teamCd, includeRetireYn));
|
||||
}
|
||||
|
||||
@PostMapping("/0010")
|
||||
public ApiResponse<Void> saveWplanList(
|
||||
@RequestBody Map<String, Object> body,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> saveList = (List<Map<String, Object>>) body.get("saveList");
|
||||
wplanService.saveWplanList(currentUser.getCorpNo(), saveList);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0020 - 나의 근무계획
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/0020")
|
||||
public ApiResponse<List<Map<String, Object>>> getMyWplanList(
|
||||
@RequestParam String workPlanYymm,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wplanService.getMyWplanList(currentUser.getCorpNo(), workPlanYymm));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0030 - 전체 근무계획
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/0030")
|
||||
public ApiResponse<List<Map<String, Object>>> getAllWplanList(
|
||||
@RequestParam String workPlanYymm,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "") String dutyCd,
|
||||
@RequestParam(defaultValue = "") String workCd,
|
||||
@RequestParam(defaultValue = "WORK_CD") String workCdType,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wplanService.getAllWplanList(
|
||||
currentUser.getCorpNo(), workPlanYymm, teamCd, dutyCd, workCd, workCdType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.company.gw.wplan.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface WplanMapper {
|
||||
|
||||
// 근무코드 목록
|
||||
List<Map<String, Object>> getWorkCodeList(Map<String, Object> param);
|
||||
|
||||
// Wplan0010 - 근무계획관리
|
||||
List<Map<String, Object>> getWplanList(Map<String, Object> param);
|
||||
|
||||
// Wplan0020 - 나의 근무계획
|
||||
List<Map<String, Object>> getMyWplanList(Map<String, Object> param);
|
||||
|
||||
// Wplan0030 - 전체 근무계획
|
||||
List<Map<String, Object>> getAllWplanList(Map<String, Object> param);
|
||||
|
||||
// 저장/삭제
|
||||
void upsertWorkplan(Map<String, Object> param);
|
||||
void deleteWorkplan(Map<String, Object> param);
|
||||
void deleteWorkplanMonth(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.company.gw.wplan.service;
|
||||
|
||||
import com.company.gw.common.util.SecurityUtil;
|
||||
import com.company.gw.wplan.mapper.WplanMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WplanService {
|
||||
|
||||
private final WplanMapper wplanMapper;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 근무코드 목록
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getWorkCodeList(String corpNo) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
return wplanMapper.getWorkCodeList(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0010 - 근무계획관리
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getWplanList(String corpNo, String workPlanYymm,
|
||||
String searchText, String teamCd,
|
||||
String includeRetireYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("workPlanYymm", workPlanYymm);
|
||||
p.put("searchText", searchText);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N");
|
||||
return wplanMapper.getWplanList(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 근무계획 일괄 저장
|
||||
* saveList: [{usrId, workPlanYymmdd, planWorkCd, sortOdr}]
|
||||
*/
|
||||
@Transactional
|
||||
public void saveWplanList(String corpNo, List<Map<String, Object>> saveList) {
|
||||
String loginUsrId = SecurityUtil.getUsrId();
|
||||
for (Map<String, Object> item : saveList) {
|
||||
Map<String, Object> p = new HashMap<>(item);
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("loginUsrId", loginUsrId);
|
||||
String planWorkCd = (String) p.get("planWorkCd");
|
||||
if (planWorkCd == null || planWorkCd.isEmpty()) {
|
||||
wplanMapper.deleteWorkplan(p);
|
||||
} else {
|
||||
wplanMapper.upsertWorkplan(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0020 - 나의 근무계획
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getMyWplanList(String corpNo, String workPlanYymm) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("usrId", SecurityUtil.getUsrId());
|
||||
p.put("workPlanYymm", workPlanYymm);
|
||||
return wplanMapper.getMyWplanList(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wplan0030 - 전체 근무계획
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getAllWplanList(String corpNo, String workPlanYymm,
|
||||
String teamCd, String dutyCd,
|
||||
String workCd, String workCdType) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("workPlanYymm", workPlanYymm);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("dutyCd", dutyCd);
|
||||
p.put("workCd", workCd);
|
||||
p.put("workCdType", workCdType != null ? workCdType : "WORK_CD");
|
||||
return wplanMapper.getAllWplanList(p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.company.gw.wtime.controller;
|
||||
|
||||
import com.company.gw.common.dto.ApiResponse;
|
||||
import com.company.gw.common.dto.CurrentUser;
|
||||
import com.company.gw.wtime.service.WtimeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/wtime")
|
||||
@RequiredArgsConstructor
|
||||
public class WtimeController {
|
||||
|
||||
private final WtimeService wtimeService;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wtime0010 - 개인별 근무시간
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/0010")
|
||||
public ApiResponse<List<Map<String, Object>>> getWtimeList(
|
||||
@RequestParam String staYmd,
|
||||
@RequestParam String endYmd,
|
||||
@RequestParam(defaultValue = "") String usrId,
|
||||
@RequestParam(defaultValue = "") String usrNm,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "") String dutyCd,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@RequestParam(defaultValue = "") String includeWorkYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wtimeService.getWtimeList(
|
||||
currentUser.getCorpNo(), staYmd, endYmd,
|
||||
usrId, usrNm, teamCd, dutyCd, includeRetireYn, includeWorkYn));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wtime0030 - 월별 근무시간 집계
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/0030")
|
||||
public ApiResponse<List<Map<String, Object>>> getWstatList(
|
||||
@RequestParam String staYmd,
|
||||
@RequestParam String endYmd,
|
||||
@RequestParam(defaultValue = "") String usrId,
|
||||
@RequestParam(defaultValue = "") String usrNm,
|
||||
@RequestParam(defaultValue = "") String teamCd,
|
||||
@RequestParam(defaultValue = "") String dutyCd,
|
||||
@RequestParam(defaultValue = "N") String includeRetireYn,
|
||||
@AuthenticationPrincipal CurrentUser currentUser) {
|
||||
return ApiResponse.ok(wtimeService.getWstatList(
|
||||
currentUser.getCorpNo(), staYmd, endYmd,
|
||||
usrId, usrNm, teamCd, dutyCd, includeRetireYn));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.company.gw.wtime.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface WtimeMapper {
|
||||
|
||||
// Wtime0010 - 개인별 근무시간
|
||||
List<Map<String, Object>> getWtimeList(Map<String, Object> param);
|
||||
|
||||
// Wtime0030 - 월별 근무시간 집계
|
||||
List<Map<String, Object>> getWstatList(Map<String, Object> param);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.company.gw.wtime.service;
|
||||
|
||||
import com.company.gw.wtime.mapper.WtimeMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WtimeService {
|
||||
|
||||
private final WtimeMapper wtimeMapper;
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wtime0010 - 개인별 근무시간
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getWtimeList(String corpNo, String staYmd, String endYmd,
|
||||
String usrId, String usrNm, String teamCd,
|
||||
String dutyCd, String includeRetireYn,
|
||||
String includeWorkYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("staYmd", staYmd);
|
||||
p.put("endYmd", endYmd);
|
||||
p.put("usrId", usrId);
|
||||
p.put("usrNm", usrNm);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("dutyCd", dutyCd);
|
||||
p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N");
|
||||
p.put("includeWorkYn", includeWorkYn);
|
||||
return wtimeMapper.getWtimeList(p);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Wtime0030 - 월별 근무시간 집계
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Map<String, Object>> getWstatList(String corpNo, String staYmd, String endYmd,
|
||||
String usrId, String usrNm, String teamCd,
|
||||
String dutyCd, String includeRetireYn) {
|
||||
Map<String, Object> p = new HashMap<>();
|
||||
p.put("corpNo", corpNo);
|
||||
p.put("staYmd", staYmd);
|
||||
p.put("endYmd", endYmd);
|
||||
p.put("usrId", usrId);
|
||||
p.put("usrNm", usrNm);
|
||||
p.put("teamCd", teamCd);
|
||||
p.put("dutyCd", dutyCd);
|
||||
p.put("includeRetireYn", includeRetireYn != null ? includeRetireYn : "N");
|
||||
return wtimeMapper.getWstatList(p);
|
||||
}
|
||||
}
|
||||
17
backend/src/main/resources/application-mariadb.yml
Normal file
17
backend/src/main/resources/application-mariadb.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# MariaDB 설정 (추후 전환용)
|
||||
spring:
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:mariadb://localhost:3306/gw}
|
||||
username: ${DB_USERNAME:}
|
||||
password: ${DB_PASSWORD:}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
mybatis:
|
||||
configuration:
|
||||
database-id: mariadb
|
||||
13
backend/src/main/resources/application-mssql.yml
Normal file
13
backend/src/main/resources/application-mssql.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
# MS SQL Server 설정 (현재 운영)
|
||||
spring:
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:sqlserver://121.156.116.136:52785;databaseName=logins_test;encrypt=false;trustServerCertificate=true;sendStringParametersAsUnicode=false}
|
||||
username: ${DB_USERNAME:logins}
|
||||
password: ${DB_PASSWORD:ghkfkdahrfh40-8}
|
||||
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
17
backend/src/main/resources/application-oracle.yml
Normal file
17
backend/src/main/resources/application-oracle.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Oracle DB 설정 (현재 운영)
|
||||
spring:
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:oracle:thin:@localhost:1521:ORCL}
|
||||
username: ${DB_USERNAME:}
|
||||
password: ${DB_PASSWORD:}
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
mybatis:
|
||||
configuration:
|
||||
database-id: oracle
|
||||
62
backend/src/main/resources/application.yml
Normal file
62
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
spring:
|
||||
application:
|
||||
name: gw-backend
|
||||
|
||||
# Active Profile: mssql | mariadb
|
||||
profiles:
|
||||
active: mssql
|
||||
|
||||
# Redis
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
# RabbitMQ
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST:localhost}
|
||||
port: ${RABBITMQ_PORT:5672}
|
||||
username: ${RABBITMQ_USER:guest}
|
||||
password: ${RABBITMQ_PASSWORD:guest}
|
||||
|
||||
# File Upload
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 50MB
|
||||
max-request-size: 100MB
|
||||
|
||||
# MyBatis
|
||||
mybatis:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
default-fetch-size: 100
|
||||
default-statement-timeout: 30
|
||||
|
||||
# JWT
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:your-secret-key-must-be-at-least-256-bits-long-for-hs256}
|
||||
access-token-expiry: 1800000 # 30분 (ms)
|
||||
refresh-token-expiry: 604800000 # 7일 (ms)
|
||||
|
||||
# File Storage
|
||||
file:
|
||||
upload-path: ${FILE_UPLOAD_PATH:/data/uploads}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level:
|
||||
com.company.gw: DEBUG
|
||||
org.mybatis: DEBUG
|
||||
85
backend/src/main/resources/logback-spring.xml
Normal file
85
backend/src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="gw-backend"/>
|
||||
|
||||
<!-- ── 패턴 ── -->
|
||||
<property name="LOG_PATTERN"
|
||||
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
|
||||
|
||||
<!-- ── 콘솔 ── -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- ── 파일 (Rolling) ── -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/${APP_NAME}/app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 날짜별 롤링, 최대 30일 보관 -->
|
||||
<fileNamePattern>/var/log/${APP_NAME}/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>3GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- ── 에러 전용 파일 ── -->
|
||||
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/${APP_NAME}/error.log</file>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>/var/log/${APP_NAME}/error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- ── 레벨 설정 ── -->
|
||||
|
||||
<!-- 로컬 개발: 콘솔만, DEBUG (mssql/mariadb는 DB 프로파일이므로 local 여부로 판단) -->
|
||||
<springProfile name="local">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
<logger name="com.company.gw" level="DEBUG"/>
|
||||
<logger name="org.mybatis" level="DEBUG"/>
|
||||
<logger name="jdbc.sqltiming" level="DEBUG"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 운영: 파일 + 콘솔, INFO -->
|
||||
<springProfile name="prod">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
<appender-ref ref="ERROR_FILE"/>
|
||||
</root>
|
||||
<logger name="com.company.gw" level="INFO"/>
|
||||
<logger name="org.mybatis" level="WARN"/>
|
||||
<logger name="org.apache.ibatis" level="WARN"/>
|
||||
</springProfile>
|
||||
|
||||
<!-- 기본 (local/prod 아닐 때) -->
|
||||
<springProfile name="!local && !prod">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
<logger name="com.company.gw" level="INFO"/>
|
||||
</springProfile>
|
||||
|
||||
</configuration>
|
||||
339
backend/src/main/resources/mapper/board/board_sql.xml
Normal file
339
backend/src/main/resources/mapper/board/board_sql.xml
Normal file
@@ -0,0 +1,339 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<!-- 원본: src/java/sql/biz/board/board0010_sql.xml
|
||||
테이블: SX_GW0020(게시물), SX_GW0030(댓글), SX_GW0010(사용자) -->
|
||||
<mapper namespace="com.company.gw.board.mapper.BoardMapper">
|
||||
|
||||
<!-- ===================== MSSQL (default) ===================== -->
|
||||
|
||||
<!-- 게시물 목록 (페이징) -->
|
||||
<select id="getPostList" parameterType="map" resultType="map">
|
||||
SELECT A.UNTY_BBS_SNO,
|
||||
A.BBS_TITLE_NM,
|
||||
A.INQR_CNT,
|
||||
A.CMMT_CNT,
|
||||
A.CTUSR_ID,
|
||||
B.USR_NM AS CTUSR_NM,
|
||||
CONVERT(VARCHAR, A.RGST_DT, 112) AS RGST_DT
|
||||
FROM SX_GW0020 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND (#{searchText} IS NULL OR #{searchText} = ''
|
||||
OR A.BBS_TITLE_NM LIKE CONCAT('%', #{searchText}, '%')
|
||||
OR CAST(A.BBS_CN AS VARCHAR(MAX)) LIKE CONCAT('%', #{searchText}, '%'))
|
||||
ORDER BY A.RGST_DT DESC, A.UNTY_BBS_SNO DESC
|
||||
OFFSET (#{pageNo} - 1) * #{pageSize} ROWS
|
||||
FETCH NEXT #{pageSize} ROWS ONLY
|
||||
</select>
|
||||
|
||||
<!-- 게시물 목록 건수 -->
|
||||
<select id="getPostListCount" parameterType="map" resultType="long">
|
||||
SELECT COUNT(1)
|
||||
FROM SX_GW0020 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND (#{searchText} IS NULL OR #{searchText} = ''
|
||||
OR A.BBS_TITLE_NM LIKE CONCAT('%', #{searchText}, '%')
|
||||
OR CAST(A.BBS_CN AS VARCHAR(MAX)) LIKE CONCAT('%', #{searchText}, '%'))
|
||||
</select>
|
||||
|
||||
<!-- 게시물 상세 -->
|
||||
<select id="getPostInfo" parameterType="map" resultType="map">
|
||||
SELECT A.UNTY_BBS_CD,
|
||||
A.UNTY_BBS_SNO,
|
||||
A.BBS_TITLE_NM,
|
||||
A.BBS_CN,
|
||||
A.INQR_CNT,
|
||||
A.CMMT_CNT,
|
||||
A.CTUSR_ID,
|
||||
A.ETC_ATCH_NO,
|
||||
A.PHOTO_ATCH_NO,
|
||||
A.RGSTR_ID,
|
||||
CONVERT(VARCHAR, A.RGST_DT, 120) AS RGST_DATE,
|
||||
CONVERT(VARCHAR, A.UPD_DT, 120) AS UPD_DATE,
|
||||
B.USR_NM AS CTUSR_NM
|
||||
FROM SX_GW0020 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND A.UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</select>
|
||||
|
||||
<!-- 게시물 등록 -->
|
||||
<insert id="insertPost" parameterType="map">
|
||||
INSERT INTO SX_GW0020 (
|
||||
CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO,
|
||||
BBS_TITLE_NM, BBS_CN, INQR_CNT,
|
||||
CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{corpNo}, #{untyBbsCd}, #{untyBbsSno},
|
||||
#{bbsTitleNm}, #{bbsCn}, 0,
|
||||
#{usrId}, #{usrId}, GETDATE(), #{usrId}, GETDATE()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 게시물 수정 -->
|
||||
<update id="updatePost" parameterType="map">
|
||||
UPDATE SX_GW0020
|
||||
SET BBS_TITLE_NM = #{bbsTitleNm},
|
||||
BBS_CN = #{bbsCn},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = GETDATE()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<!-- 조회수 증가 -->
|
||||
<update id="increaseInqrCnt" parameterType="map">
|
||||
UPDATE SX_GW0020
|
||||
SET INQR_CNT = ISNULL(INQR_CNT, 0) + 1
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<!-- 기타첨부 번호 업데이트 -->
|
||||
<update id="updateEtcAtchNo" parameterType="map">
|
||||
UPDATE SX_GW0020
|
||||
SET ETC_ATCH_NO = #{etcAtchNo},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = GETDATE()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<!-- 사진첨부 번호 업데이트 -->
|
||||
<update id="updatePhotoAtchNo" parameterType="map">
|
||||
UPDATE SX_GW0020
|
||||
SET PHOTO_ATCH_NO = #{photoAtchNo},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = GETDATE()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<!-- 댓글수 재계산 -->
|
||||
<update id="updateCmmtCnt" parameterType="map">
|
||||
UPDATE SX_GW0020
|
||||
SET CMMT_CNT = (
|
||||
SELECT COUNT(1) FROM SX_GW0030
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
),
|
||||
MODID = #{usrId},
|
||||
UPD_DT = GETDATE()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<!-- 게시물 삭제 -->
|
||||
<delete id="deletePost" parameterType="map">
|
||||
DELETE FROM SX_GW0020
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</delete>
|
||||
|
||||
<!-- 댓글 목록 -->
|
||||
<select id="getCommentList" parameterType="map" resultType="map">
|
||||
SELECT A.CMMT_SNO,
|
||||
A.CMMT_CN,
|
||||
A.CMMT_CTUSR_ID,
|
||||
B.USR_NM AS CMMT_CTUSR_NM,
|
||||
CONVERT(VARCHAR, A.RGST_DT, 120) AS RGST_DATE
|
||||
FROM SX_GW0030 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CMMT_CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND A.UNTY_BBS_SNO = #{untyBbsSno}
|
||||
ORDER BY A.RGST_DT, A.CMMT_SNO
|
||||
</select>
|
||||
|
||||
<!-- 댓글 상세 -->
|
||||
<select id="getCommentInfo" parameterType="map" resultType="map">
|
||||
SELECT A.CMMT_SNO, A.CMMT_CN, A.CMMT_CTUSR_ID
|
||||
FROM SX_GW0030 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND A.UNTY_BBS_SNO = #{untyBbsSno}
|
||||
AND A.CMMT_SNO = #{cmmtSno}
|
||||
</select>
|
||||
|
||||
<!-- 댓글 등록 -->
|
||||
<insert id="insertComment" parameterType="map">
|
||||
INSERT INTO SX_GW0030 (
|
||||
CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, CMMT_SNO,
|
||||
CMMT_CN, CMMT_CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{corpNo}, #{untyBbsCd}, #{untyBbsSno}, #{cmmtSno},
|
||||
#{cmmtCn}, #{usrId}, #{usrId}, GETDATE(), #{usrId}, GETDATE()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 댓글 삭제 -->
|
||||
<delete id="deleteComment" parameterType="map">
|
||||
DELETE FROM SX_GW0030
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
AND CMMT_SNO = #{cmmtSno}
|
||||
</delete>
|
||||
|
||||
<!-- 게시물 전체 댓글 삭제 (게시물 삭제시) -->
|
||||
<delete id="deleteCommentsByPost" parameterType="map">
|
||||
DELETE FROM SX_GW0030
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</delete>
|
||||
|
||||
<!-- ===================== MariaDB ===================== -->
|
||||
|
||||
<select id="getPostList" parameterType="map" resultType="map" databaseId="mariadb">
|
||||
SELECT A.UNTY_BBS_SNO,
|
||||
A.BBS_TITLE_NM,
|
||||
A.INQR_CNT,
|
||||
A.CMMT_CNT,
|
||||
A.CTUSR_ID,
|
||||
B.USR_NM AS CTUSR_NM,
|
||||
DATE_FORMAT(A.RGST_DT, '%Y%m%d') AS RGST_DT
|
||||
FROM SX_GW0020 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND (#{searchText} IS NULL OR #{searchText} = ''
|
||||
OR A.BBS_TITLE_NM LIKE CONCAT('%', #{searchText}, '%')
|
||||
OR CAST(A.BBS_CN AS CHAR) LIKE CONCAT('%', #{searchText}, '%'))
|
||||
ORDER BY A.RGST_DT DESC, A.UNTY_BBS_SNO DESC
|
||||
LIMIT #{pageSize} OFFSET (#{pageNo} - 1) * #{pageSize}
|
||||
</select>
|
||||
|
||||
<select id="getPostListCount" parameterType="map" resultType="long" databaseId="mariadb">
|
||||
SELECT COUNT(1)
|
||||
FROM SX_GW0020 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND (#{searchText} IS NULL OR #{searchText} = ''
|
||||
OR A.BBS_TITLE_NM LIKE CONCAT('%', #{searchText}, '%')
|
||||
OR CAST(A.BBS_CN AS CHAR) LIKE CONCAT('%', #{searchText}, '%'))
|
||||
</select>
|
||||
|
||||
<select id="getPostInfo" parameterType="map" resultType="map" databaseId="mariadb">
|
||||
SELECT A.UNTY_BBS_CD,
|
||||
A.UNTY_BBS_SNO,
|
||||
A.BBS_TITLE_NM,
|
||||
A.BBS_CN,
|
||||
A.INQR_CNT,
|
||||
A.CMMT_CNT,
|
||||
A.CTUSR_ID,
|
||||
A.ETC_ATCH_NO,
|
||||
A.PHOTO_ATCH_NO,
|
||||
A.RGSTR_ID,
|
||||
DATE_FORMAT(A.RGST_DT, '%Y-%m-%d %H:%i:%s') AS RGST_DATE,
|
||||
DATE_FORMAT(A.UPD_DT, '%Y-%m-%d %H:%i:%s') AS UPD_DATE,
|
||||
B.USR_NM AS CTUSR_NM
|
||||
FROM SX_GW0020 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND A.UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</select>
|
||||
|
||||
<insert id="insertPost" parameterType="map" databaseId="mariadb">
|
||||
INSERT INTO SX_GW0020 (
|
||||
CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO,
|
||||
BBS_TITLE_NM, BBS_CN, INQR_CNT,
|
||||
CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{corpNo}, #{untyBbsCd}, #{untyBbsSno},
|
||||
#{bbsTitleNm}, #{bbsCn}, 0,
|
||||
#{usrId}, #{usrId}, NOW(), #{usrId}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updatePost" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_GW0020
|
||||
SET BBS_TITLE_NM = #{bbsTitleNm},
|
||||
BBS_CN = #{bbsCn},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = NOW()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<update id="increaseInqrCnt" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_GW0020
|
||||
SET INQR_CNT = IFNULL(INQR_CNT, 0) + 1
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<update id="updateEtcAtchNo" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_GW0020
|
||||
SET ETC_ATCH_NO = #{etcAtchNo},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = NOW()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<update id="updatePhotoAtchNo" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_GW0020
|
||||
SET PHOTO_ATCH_NO = #{photoAtchNo},
|
||||
MODID = #{usrId},
|
||||
UPD_DT = NOW()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<update id="updateCmmtCnt" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_GW0020
|
||||
SET CMMT_CNT = (
|
||||
SELECT COUNT(1) FROM SX_GW0030
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
),
|
||||
MODID = #{usrId},
|
||||
UPD_DT = NOW()
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
AND UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND UNTY_BBS_SNO = #{untyBbsSno}
|
||||
</update>
|
||||
|
||||
<insert id="insertComment" parameterType="map" databaseId="mariadb">
|
||||
INSERT INTO SX_GW0030 (
|
||||
CORP_NO, UNTY_BBS_CD, UNTY_BBS_SNO, CMMT_SNO,
|
||||
CMMT_CN, CMMT_CTUSR_ID, RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{corpNo}, #{untyBbsCd}, #{untyBbsSno}, #{cmmtSno},
|
||||
#{cmmtCn}, #{usrId}, #{usrId}, NOW(), #{usrId}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="getCommentList" parameterType="map" resultType="map" databaseId="mariadb">
|
||||
SELECT A.CMMT_SNO,
|
||||
A.CMMT_CN,
|
||||
A.CMMT_CTUSR_ID,
|
||||
B.USR_NM AS CMMT_CTUSR_NM,
|
||||
DATE_FORMAT(A.RGST_DT, '%Y-%m-%d %H:%i:%s') AS RGST_DATE
|
||||
FROM SX_GW0030 A
|
||||
LEFT JOIN SX_GW0010 B ON B.CORP_NO = A.CORP_NO AND B.USR_ID = A.CMMT_CTUSR_ID
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.UNTY_BBS_CD = #{untyBbsCd}
|
||||
AND A.UNTY_BBS_SNO = #{untyBbsSno}
|
||||
ORDER BY A.RGST_DT, A.CMMT_SNO
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
111
backend/src/main/resources/mapper/common/attach_sql.xml
Normal file
111
backend/src/main/resources/mapper/common/attach_sql.xml
Normal file
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.company.gw.common.mapper.AttachMapper">
|
||||
|
||||
<!-- ===================== MSSQL (default) ===================== -->
|
||||
|
||||
<select id="getFileInfo" parameterType="string" resultType="map">
|
||||
SELECT ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, TBL_COL_NM, RGSTR_ID
|
||||
FROM SX_CO0050
|
||||
WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</select>
|
||||
|
||||
<select id="getFileInfoListByAtchNo" parameterType="string" resultType="map">
|
||||
SELECT ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, TBL_COL_NM
|
||||
FROM SX_CO0050
|
||||
WHERE ATCH_NO = #{atchNo}
|
||||
AND DEL_DT IS NULL
|
||||
ORDER BY RGST_DT
|
||||
</select>
|
||||
|
||||
<insert id="insertFileInfo" parameterType="map">
|
||||
INSERT INTO SX_CO0050 (
|
||||
ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, DEL_DT, TBL_COL_NM,
|
||||
RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{atchfileNo}, #{atchNo}, #{atchFilePathNm}, #{atchFileNm},
|
||||
#{atchTypeNm}, #{atchFileMg}, NULL, #{tblColNm},
|
||||
#{rgstrId}, GETDATE(), #{rgstrId}, GETDATE()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<delete id="deleteFileInfo" parameterType="string">
|
||||
DELETE FROM SX_CO0050 WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</delete>
|
||||
|
||||
<update id="updateTblColNmByAtchNo" parameterType="map">
|
||||
UPDATE SX_CO0050
|
||||
SET TBL_COL_NM = #{tblColNm}
|
||||
WHERE ATCH_NO = #{atchNo}
|
||||
</update>
|
||||
|
||||
<update id="updateTblColNmByAtchfileNo" parameterType="map">
|
||||
UPDATE SX_CO0050
|
||||
SET TBL_COL_NM = #{tblColNm}
|
||||
WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</update>
|
||||
|
||||
<select id="getNeedDeleteFileList" resultType="map">
|
||||
SELECT ATCHFILE_NO, ATCH_FILE_PATH_NM
|
||||
FROM SX_CO0050
|
||||
WHERE NULLIF(TBL_COL_NM, '') IS NULL
|
||||
AND RGST_DT < DATEADD(HOUR, -6, GETDATE())
|
||||
</select>
|
||||
|
||||
<!-- ===================== MariaDB ===================== -->
|
||||
|
||||
<select id="getFileInfo" parameterType="string" resultType="map" databaseId="mariadb">
|
||||
SELECT ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, TBL_COL_NM, RGSTR_ID
|
||||
FROM SX_CO0050
|
||||
WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</select>
|
||||
|
||||
<select id="getFileInfoListByAtchNo" parameterType="string" resultType="map" databaseId="mariadb">
|
||||
SELECT ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, TBL_COL_NM
|
||||
FROM SX_CO0050
|
||||
WHERE ATCH_NO = #{atchNo}
|
||||
AND DEL_DT IS NULL
|
||||
ORDER BY RGST_DT
|
||||
</select>
|
||||
|
||||
<insert id="insertFileInfo" parameterType="map" databaseId="mariadb">
|
||||
INSERT INTO SX_CO0050 (
|
||||
ATCHFILE_NO, ATCH_NO, ATCH_FILE_PATH_NM, ATCH_FILE_NM,
|
||||
ATCH_TYPE_NM, ATCH_FILE_MG, DEL_DT, TBL_COL_NM,
|
||||
RGSTR_ID, RGST_DT, MODID, UPD_DT
|
||||
) VALUES (
|
||||
#{atchfileNo}, #{atchNo}, #{atchFilePathNm}, #{atchFileNm},
|
||||
#{atchTypeNm}, #{atchFileMg}, NULL, #{tblColNm},
|
||||
#{rgstrId}, NOW(), #{rgstrId}, NOW()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<delete id="deleteFileInfo" parameterType="string" databaseId="mariadb">
|
||||
DELETE FROM SX_CO0050 WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</delete>
|
||||
|
||||
<update id="updateTblColNmByAtchNo" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_CO0050
|
||||
SET TBL_COL_NM = #{tblColNm}
|
||||
WHERE ATCH_NO = #{atchNo}
|
||||
</update>
|
||||
|
||||
<update id="updateTblColNmByAtchfileNo" parameterType="map" databaseId="mariadb">
|
||||
UPDATE SX_CO0050
|
||||
SET TBL_COL_NM = #{tblColNm}
|
||||
WHERE ATCHFILE_NO = #{atchfileNo}
|
||||
</update>
|
||||
|
||||
<select id="getNeedDeleteFileList" resultType="map" databaseId="mariadb">
|
||||
SELECT ATCHFILE_NO, ATCH_FILE_PATH_NM
|
||||
FROM SX_CO0050
|
||||
WHERE (TBL_COL_NM IS NULL OR TBL_COL_NM = '')
|
||||
AND RGST_DT < DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
145
backend/src/main/resources/mapper/common/code_sql.xml
Normal file
145
backend/src/main/resources/mapper/common/code_sql.xml
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<!--
|
||||
공통코드 조회 SQL
|
||||
테이블: SX_CO0040 (공통코드), SX_GW0010 (직원), SX_CO0070 (근무코드)
|
||||
-->
|
||||
<mapper namespace="com.company.gw.common.mapper.CodeMapper">
|
||||
|
||||
<!-- ──────────────────────────────────────────────
|
||||
공통코드 목록 (드롭다운용 간략버전)
|
||||
────────────────────────────────────────────── -->
|
||||
<select id="getCodeList" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.COMM_CD AS code,
|
||||
A.COMM_CD_NM AS name,
|
||||
A.COMM_CD_USE_YN AS useYn
|
||||
FROM SX_CO0040 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.COMM_CL_CD = #{commClCd}
|
||||
AND (ISNULL(#{useYn}, '') = '' OR A.COMM_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.COMM_CD_DSPLY_ORDR, A.COMM_CD
|
||||
]]></select>
|
||||
|
||||
<select id="getCodeList" databaseId="mariadb" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.COMM_CD AS code,
|
||||
A.COMM_CD_NM AS name,
|
||||
A.COMM_CD_USE_YN AS useYn
|
||||
FROM SX_CO0040 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.COMM_CL_CD = #{commClCd}
|
||||
AND (IFNULL(#{useYn}, '') = '' OR A.COMM_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.COMM_CD_DSPLY_ORDR, A.COMM_CD
|
||||
]]></select>
|
||||
|
||||
<!-- ──────────────────────────────────────────────
|
||||
공통코드 전체 정보
|
||||
────────────────────────────────────────────── -->
|
||||
<select id="getCodeListFull" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.COMM_CD AS code,
|
||||
A.COMM_CD_NM AS name,
|
||||
A.COMM_CL_CD,
|
||||
A.COMM_CD_DSPLY_ORDR,
|
||||
A.COMM_CD_USE_YN,
|
||||
A.COMM_CD_TYPE_VAL,
|
||||
A.COMM_CD_DSCRPT,
|
||||
A.PROP_CD1,
|
||||
A.PROP_CD2,
|
||||
A.PROP_CD3,
|
||||
A.PROP_CD4,
|
||||
A.PROP_CD5
|
||||
FROM SX_CO0040 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.COMM_CL_CD = #{commClCd}
|
||||
AND (ISNULL(#{useYn}, '') = '' OR A.COMM_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.COMM_CD_DSPLY_ORDR, A.COMM_CD
|
||||
]]></select>
|
||||
|
||||
<select id="getCodeListFull" databaseId="mariadb" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.COMM_CD AS code,
|
||||
A.COMM_CD_NM AS name,
|
||||
A.COMM_CL_CD,
|
||||
A.COMM_CD_DSPLY_ORDR,
|
||||
A.COMM_CD_USE_YN,
|
||||
A.COMM_CD_TYPE_VAL,
|
||||
A.COMM_CD_DSCRPT,
|
||||
A.PROP_CD1,
|
||||
A.PROP_CD2,
|
||||
A.PROP_CD3,
|
||||
A.PROP_CD4,
|
||||
A.PROP_CD5
|
||||
FROM SX_CO0040 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.COMM_CL_CD = #{commClCd}
|
||||
AND (IFNULL(#{useYn}, '') = '' OR A.COMM_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.COMM_CD_DSPLY_ORDR, A.COMM_CD
|
||||
]]></select>
|
||||
|
||||
<!-- ──────────────────────────────────────────────
|
||||
직원 목록 (결재자 선택 등 팝업용)
|
||||
────────────────────────────────────────────── -->
|
||||
<select id="searchUser" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT TOP 100
|
||||
A.USR_ID,
|
||||
A.USR_NM,
|
||||
A.DUTY_CD,
|
||||
A.TEAM_CD,
|
||||
A.APPR_YN
|
||||
FROM SX_GW0010 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND ISNULL(A.RETIREMENT_DATE, '') = ''
|
||||
AND (ISNULL(#{searchText}, '') = '' OR (
|
||||
A.USR_ID LIKE CONCAT('%', #{searchText}, '%') OR
|
||||
A.USR_NM LIKE CONCAT('%', #{searchText}, '%')))
|
||||
AND (#{apprOnly} != 'Y' OR A.APPR_YN = 'Y')
|
||||
ORDER BY A.USR_NM
|
||||
]]></select>
|
||||
|
||||
<select id="searchUser" databaseId="mariadb" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.USR_ID,
|
||||
A.USR_NM,
|
||||
A.DUTY_CD,
|
||||
A.TEAM_CD,
|
||||
A.APPR_YN
|
||||
FROM SX_GW0010 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND IFNULL(A.RETIREMENT_DATE, '') = ''
|
||||
AND (IFNULL(#{searchText}, '') = '' OR (
|
||||
A.USR_ID LIKE CONCAT('%', #{searchText}, '%') OR
|
||||
A.USR_NM LIKE CONCAT('%', #{searchText}, '%')))
|
||||
AND (#{apprOnly} != 'Y' OR A.APPR_YN = 'Y')
|
||||
ORDER BY A.USR_NM
|
||||
LIMIT 100
|
||||
]]></select>
|
||||
|
||||
<!-- ──────────────────────────────────────────────
|
||||
근무코드 목록
|
||||
────────────────────────────────────────────── -->
|
||||
<select id="getWorkCdList" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.WORK_CD AS code,
|
||||
A.WORK_CD_TITLE_NM AS name,
|
||||
A.WORK_CD,
|
||||
A.WORK_CD_TITLE_NM,
|
||||
A.GOTOWORK_TM_NM,
|
||||
A.GETOFFWORK_TM_NM,
|
||||
A.WORK_CD_USE_YN
|
||||
FROM SX_CO0070 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND (ISNULL(#{useYn}, '') = '' OR A.WORK_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.WORK_CD
|
||||
]]></select>
|
||||
|
||||
<select id="getWorkCdList" databaseId="mariadb" parameterType="map" resultType="map"><![CDATA[
|
||||
SELECT A.WORK_CD AS code,
|
||||
A.WORK_CD_TITLE_NM AS name,
|
||||
A.WORK_CD,
|
||||
A.WORK_CD_TITLE_NM,
|
||||
A.GOTOWORK_TM_NM,
|
||||
A.GETOFFWORK_TM_NM,
|
||||
A.WORK_CD_USE_YN
|
||||
FROM SX_CO0070 A
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND (IFNULL(#{useYn}, '') = '' OR A.WORK_CD_USE_YN = #{useYn})
|
||||
ORDER BY A.WORK_CD
|
||||
]]></select>
|
||||
|
||||
</mapper>
|
||||
28
backend/src/main/resources/mapper/common/common_sql.xml
Normal file
28
backend/src/main/resources/mapper/common/common_sql.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<!-- 원본: src/java/sql/common/common_sql.xml -->
|
||||
<mapper namespace="com.company.gw.common.mapper.CommonMapper">
|
||||
|
||||
<!-- 시퀀스 증가 (MERGE = SQL Server / MariaDB INSERT ON DUPLICATE KEY) -->
|
||||
<update id="increaseSequence" parameterType="String">
|
||||
MERGE INTO SX_CO0060 T
|
||||
USING (SELECT 1 AS DUMMY) S
|
||||
ON (T.SEQUENCE_NM = #{sequenceNm})
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (SEQUENCE_NM, SEQUENCE_VAL) VALUES (#{sequenceNm}, 1)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET SEQUENCE_VAL = SEQUENCE_VAL + 1;
|
||||
</update>
|
||||
|
||||
<update id="increaseSequence" parameterType="String" databaseId="mariadb">
|
||||
INSERT INTO SX_CO0060 (SEQUENCE_NM, SEQUENCE_VAL)
|
||||
VALUES (#{sequenceNm}, 1)
|
||||
ON DUPLICATE KEY UPDATE SEQUENCE_VAL = SEQUENCE_VAL + 1
|
||||
</update>
|
||||
|
||||
<select id="getSequenceVal" parameterType="String" resultType="Long">
|
||||
SELECT SEQUENCE_VAL FROM SX_CO0060 WHERE SEQUENCE_NM = #{sequenceNm}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
146
backend/src/main/resources/mapper/common/menu_sql.xml
Normal file
146
backend/src/main/resources/mapper/common/menu_sql.xml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<!--
|
||||
원본: src/java/sql/common/menu_sql.xml (SX_CO0080, SX_CO0090)
|
||||
-->
|
||||
<mapper namespace="com.company.gw.common.mapper.MenuMapper">
|
||||
|
||||
<!-- 사용자 권한 기반 메뉴 트리 조회 (재귀 CTE) -->
|
||||
<select id="getMenuList" parameterType="map" resultType="com.company.gw.common.dto.MenuDto">
|
||||
WITH MENU_TREE AS (
|
||||
SELECT A.MENU_NO AS upMenuNo,
|
||||
A.CORP_NO,
|
||||
A.MENU_NO,
|
||||
A.UPPER_MENU_NO,
|
||||
A.MENU_NM,
|
||||
A.URL,
|
||||
A.RM,
|
||||
A.MENU_PROP,
|
||||
A.MENU_ORDR,
|
||||
A.MENU_USE_YN,
|
||||
1 AS lvl,
|
||||
CAST('/' + RIGHT('00000' + CONVERT(VARCHAR, A.MENU_ORDR), 5) AS VARCHAR(MAX)) AS sortPath
|
||||
FROM SX_CO0080 A
|
||||
JOIN SX_CO0090 B
|
||||
ON B.CORP_NO = A.CORP_NO
|
||||
AND B.MENU_NO = A.MENU_NO
|
||||
AND B.MENU_AUTH_CD = #{menuAuthCd}
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.MENU_USE_YN = 'Y'
|
||||
AND NULLIF(A.UPPER_MENU_NO, '') IS NULL
|
||||
UNION ALL
|
||||
SELECT PARENT.upMenuNo,
|
||||
A.CORP_NO,
|
||||
A.MENU_NO,
|
||||
A.UPPER_MENU_NO,
|
||||
A.MENU_NM,
|
||||
A.URL,
|
||||
A.RM,
|
||||
A.MENU_PROP,
|
||||
A.MENU_ORDR,
|
||||
A.MENU_USE_YN,
|
||||
PARENT.lvl + 1 AS lvl,
|
||||
PARENT.sortPath + CAST('/' + RIGHT('00000' + CONVERT(VARCHAR, A.MENU_ORDR), 5) AS VARCHAR(MAX)) AS sortPath
|
||||
FROM SX_CO0080 A
|
||||
JOIN SX_CO0090 B
|
||||
ON B.CORP_NO = A.CORP_NO
|
||||
AND B.MENU_NO = A.MENU_NO
|
||||
AND B.MENU_AUTH_CD = #{menuAuthCd}
|
||||
JOIN MENU_TREE PARENT
|
||||
ON PARENT.CORP_NO = A.CORP_NO
|
||||
AND PARENT.MENU_NO = A.UPPER_MENU_NO
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.MENU_USE_YN = 'Y'
|
||||
)
|
||||
SELECT MENU_NO AS menuNo,
|
||||
UPPER_MENU_NO AS upperMenuNo,
|
||||
MENU_NM AS menuNm,
|
||||
URL,
|
||||
RM,
|
||||
MENU_PROP AS menuProp,
|
||||
MENU_ORDR AS menuOrdr,
|
||||
MENU_USE_YN AS menuUseYn,
|
||||
lvl
|
||||
FROM MENU_TREE
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
ORDER BY sortPath
|
||||
</select>
|
||||
|
||||
<select id="getMenuList" parameterType="map" resultType="com.company.gw.common.dto.MenuDto" databaseId="mariadb">
|
||||
WITH MENU_TREE AS (
|
||||
SELECT A.MENU_NO AS upMenuNo,
|
||||
A.CORP_NO,
|
||||
A.MENU_NO,
|
||||
A.UPPER_MENU_NO,
|
||||
A.MENU_NM,
|
||||
A.URL,
|
||||
A.RM,
|
||||
A.MENU_PROP,
|
||||
A.MENU_ORDR,
|
||||
A.MENU_USE_YN,
|
||||
1 AS lvl,
|
||||
CONCAT('/', LPAD(A.MENU_ORDR, 5, '0')) AS sortPath
|
||||
FROM SX_CO0080 A
|
||||
JOIN SX_CO0090 B
|
||||
ON B.CORP_NO = A.CORP_NO
|
||||
AND B.MENU_NO = A.MENU_NO
|
||||
AND B.MENU_AUTH_CD = #{menuAuthCd}
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.MENU_USE_YN = 'Y'
|
||||
AND NULLIF(A.UPPER_MENU_NO, '') IS NULL
|
||||
UNION ALL
|
||||
SELECT PARENT.upMenuNo,
|
||||
A.CORP_NO,
|
||||
A.MENU_NO,
|
||||
A.UPPER_MENU_NO,
|
||||
A.MENU_NM,
|
||||
A.URL,
|
||||
A.RM,
|
||||
A.MENU_PROP,
|
||||
A.MENU_ORDR,
|
||||
A.MENU_USE_YN,
|
||||
PARENT.lvl + 1 AS lvl,
|
||||
CONCAT(PARENT.sortPath, '/', LPAD(A.MENU_ORDR, 5, '0')) AS sortPath
|
||||
FROM SX_CO0080 A
|
||||
JOIN SX_CO0090 B
|
||||
ON B.CORP_NO = A.CORP_NO
|
||||
AND B.MENU_NO = A.MENU_NO
|
||||
AND B.MENU_AUTH_CD = #{menuAuthCd}
|
||||
JOIN MENU_TREE PARENT
|
||||
ON PARENT.CORP_NO = A.CORP_NO
|
||||
AND PARENT.MENU_NO = A.UPPER_MENU_NO
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.MENU_USE_YN = 'Y'
|
||||
)
|
||||
SELECT MENU_NO AS menuNo,
|
||||
UPPER_MENU_NO AS upperMenuNo,
|
||||
MENU_NM AS menuNm,
|
||||
URL,
|
||||
RM,
|
||||
MENU_PROP AS menuProp,
|
||||
MENU_ORDR AS menuOrdr,
|
||||
MENU_USE_YN AS menuUseYn,
|
||||
lvl
|
||||
FROM MENU_TREE
|
||||
WHERE CORP_NO = #{corpNo}
|
||||
ORDER BY sortPath
|
||||
</select>
|
||||
|
||||
<!-- 컨트롤러-롤 매핑 목록 (접근 권한 체크용) -->
|
||||
<select id="getControllerRoleList" parameterType="map" resultType="com.company.gw.common.dto.ControllerRoleDto">
|
||||
SELECT DISTINCT
|
||||
A.CONTROLLER AS controller,
|
||||
B.MENU_AUTH_CD AS role,
|
||||
B.FUNC_AUTH_CN AS funcAuthCn
|
||||
FROM SX_CO0080 A
|
||||
JOIN SX_CO0090 B
|
||||
ON B.CORP_NO = A.CORP_NO
|
||||
AND B.MENU_NO = A.MENU_NO
|
||||
WHERE A.CORP_NO = #{corpNo}
|
||||
AND A.MENU_USE_YN = 'Y'
|
||||
AND NULLIF(A.CONTROLLER, '') IS NOT NULL
|
||||
ORDER BY A.CONTROLLER
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user