admin/front 분리

This commit is contained in:
2026-07-20 17:49:24 +09:00
parent 9269278e40
commit ed41ef24cb
673 changed files with 16782 additions and 11277 deletions
+52
View File
@@ -12,7 +12,59 @@
- 사용자가 IDE 에서 파일을 수정하거나 새 파일을 만든 뒤 질문할 수 있으므로, 관련 답변 전에는 저장된 최신 파일 상태를 다시 확인하는 것을 우선한다.
- 이전에 읽은 세션 문맥만으로 최신 파일 상태를 단정하지 말고, 저장되지 않은 편집 내용은 확인할 수 없음을 전제로 설명한다.
## 작업 진행 표시
- 기능 또는 모듈 단위 작업에서만 진행 상태를 표시한다.
- 시작 전에는 최대 4개 단계로 나누고, 현재 완료 기준으로 퍼센트를 표시한다.
- 완료는 `✓`, 진행 중은 `●`, 예정은 `○`로 표시한다.
- 퍼센트는 시간 예측이 아니라 실제 완료한 단계 기준으로 표시한다.
- 컴파일, 테스트, 실제 연동 검증은 필요한 경우 별도 단계로 표시한다.
- Redis, 파일, 외부 API 등 기능이 사용하지 않는 의존성 검증 단계는 표시하지 않는다.
- 단순 파일 확인이나 검색에는 진행 상태를 표시하지 않는다.
- 오류가 발생하면 해당 단계에 머물고 원인과 다음 조치를 짧게 공유한다.
## 모델 라우팅 규칙
- Terra는 현재 작업의 총괄 역할을 맡는다. 구조 설계, 레거시 분석, 보안·권한·트랜잭션·Redis 판단, 오류 원인 분석, 코드 리뷰와 실제 연동 검증은 Terra가 처리한다.
- Luna는 범위가 명확하고 독립적인 반복 작업을 맡는다. DTO·VO·Mapper 보일러플레이트 작성, 단순 CRUD 이관, 컴파일·단위 테스트 실행, 포맷과 정적 확인이 대상이다.
- 한 대화 안에서 모델을 교체하는 대신, Terra가 필요한 작업만 Luna 하위 작업으로 위임하고 결과를 다시 검토한다.
- 같은 파일 또는 강하게 연결된 파일을 동시에 수정하는 작업은 충돌 방지를 위해 Terra가 직접 처리한다.
- 보안, 데이터 손상 가능성, 다중 모듈 영향, 실제 DB·Redis·외부 연동이 포함되면 Luna 단독 처리 대상으로 보내지 않는다.
- 모델 라우팅은 작업의 난이도와 위험도로 판단한다. 단순 반복이라는 이유만으로 도메인 정책이나 DB 변경 판단까지 위임하지 않는다.
- 추론 강도는 Terra와 Luna 모두 기본 `medium`으로 사용한다.
- 비용과 응답 속도를 우선하므로 `high` 이상의 추론 강도는 사용하지 않는다. 더 깊은 판단이 필요하면 Terra가 범위를 나누어 확인하고 결과를 단계적으로 공유한다.
### 작업별 담당
| 작업 | 기본 담당 | 기준 |
|---|---|---|
| 실행 모듈, 업무 패키지, API 구조 설계 | Terra | `admin`, `front`, `core``standard`, `bespoke`의 경계를 판단한다. |
| 레거시 분석과 이관 범위 결정 | Terra | 기존 정책, 누락 기능, 호환성을 해석한다. |
| 단일·복합 SQL 분류, 트랜잭션과 응답 코드 결정 | Terra | 재사용성과 데이터 정합성을 판단한다. |
| JWT, Security, 권한, CORS, Redis, 파일, 외부 연동 | Terra | 보안·운영 영향이 크다. |
| 확정된 standard CRUD DTO·VO·Mapper·XML 작성 | Luna | 이미 정해진 계약을 반복 적용한다. |
| 확정된 Form, 응답 VO, Swagger 설명 작성 | Luna | API 필드와 문구가 확정된 경우에 한한다. |
| import, 네이밍, 포맷, XML namespace/id 점검 | Luna | 독립적이고 저위험인 기계적 확인이다. |
| compileJava, 단위 테스트 실행 | Luna | 빠른 반복 검증을 우선한다. |
| 통합·실제 연동 테스트 시나리오와 결과 판단 | Terra | Security, DB, Redis, 외부 의존성의 영향 범위를 판단한다. |
| 오류 원인 분석과 수정 방향 결정 | Terra | 로그 해석을 넘어 구조와 정책을 확인한다. |
### 적용 흐름
```text
Terra: 구조, SQL 분류, 정책, 테스트 조건 결정
-> Luna: 확정된 계약의 반복 구현과 빠른 컴파일·단위 검증
-> Terra: 결과 검토, 통합·실제 연동 검증, 다음 수정 방향 결정
```
- 판단 기준은 한 문장으로 정리한다. 새 설계·정책 판단이 필요하면 Terra, 이미 정해진 계약을 여러 파일에 적용하면 Luna다.
- Luna가 작성한 결과도 Terra가 기존 구조, 보안 규칙, 실제 의존성 영향과 함께 검토한 뒤 반영한다.
## 사용자 선호 규칙
- 신규 API 또는 기존 기능 이관 작업은 코드나 파일 생성 전에 먼저 패키지와 메서드 구성을 트리 구조로 제시한다. 이 단계에서는 구현 코드를 먼저 보여주지 않는다.
- 작업 전 구조 제시는 `실행 모듈 -> 업무 1 depth 패키지 -> Controller API 메서드 -> 업무 Service 메서드 -> Form/VO -> core standard/bespoke Service/Mapper/핵심 메서드` 순서로 작성한다.
- 구조 제시 단계에서 core standard와 bespoke의 책임을 함께 구분한다. 단일 테이블 CRUD와 전용 조회는 standard, JOIN·집계·복합 SQL은 bespoke로 표시한다.
- 사용자가 구조와 메서드 구성을 확인한 뒤에만 코드 제시 또는 직접 작성을 진행한다.
- 소스코드 변경이 필요한 요청에서는 바로 구현하지 말고 먼저 다음 중 무엇을 원하는지 확인한다.
- `코드로 보여주기`: 사용자가 직접 프로젝트에 반영할 수 있도록 예시 코드나 패치를 제공한다.
- `직접 작성하기`: 에이전트가 저장소에 직접 수정한다.
+264
View File
@@ -0,0 +1,264 @@
# 구조 한눈에 보기
이 문서는 프로젝트에 처음 들어온 사람이 `core`, `admin`, `front`가 왜 나뉘어 있는지와 새 코드를 어느 위치에 만들어야 하는지를 빠르게 이해하기 위한 안내서다.
## 1. 전체 구조
```text
한 Git 저장소
|
|-- 01-api-core 공유 라이브러리, 단독 실행 안 함
|-- 02-api-admin 관리자 API 서버, 별도 JAR 배포
`-- 03-api-front 사용자 API 서버, 별도 JAR 배포
```
```text
브라우저 또는 외부 클라이언트
|
v
Controller (admin 또는 front)
|
v
업무 Service (admin 또는 front)
|
+--> core standard Service --> Mapper --> Mapper XML --> DB
|
`--> core bespoke Service --> Mapper --> Mapper XML --> DB
```
`02-api-admin``03-api-front`는 각각 독립 실행된다. 따라서 admin JAR에는 front Controller가, front JAR에는 admin Controller가 들어가지 않는다. 두 서버가 공통으로 사용하는 코드는 `01-api-core`에 둔다.
## 2. 어디에 무엇을 두는가
| 위치 | 책임 | 두면 안 되는 것 |
|---|---|---|
| `core/common` | JWT, 예외, 공통 응답, 페이징, 유틸 | 화면별 정책, Controller |
| `core/config` | DataSource, MyBatis, 공통 인프라 설정 | admin/front 화면 전용 설정 |
| `core/modules/standard` | 한 테이블의 조회, 등록, 수정, 삭제 | 여러 테이블 JOIN SQL |
| `core/modules/bespoke` | JOIN, 집계, `INSERT ... SELECT`, 예외적인 복합 SQL | 단일 테이블 CRUD 중복 |
| `admin/modules` | 관리자 Controller, 업무 흐름, Form, 응답 VO | front Controller |
| `front/modules` | 사용자 Controller, 업무 흐름, Form, 응답 VO | admin Controller |
admin/front의 `modules` 바로 아래 1 depth는 테이블명이 아니라 서비스의 업무 메뉴 기준이다. 예를 들어 백오피스 메뉴 아래에는 관리자 계정, 권한, 메뉴 관리 기능이 함께 있으므로 모두 `backOffice` 모듈에 둔다. 일반 회원 기능이 필요해져도 백오피스의 관리자 계정 관리와 섞지 않는다.
### standard와 bespoke 판단법
```text
SQL이 한 테이블만 접근하는가?
|- 예: standard
`- 아니오: bespoke
```
예시:
```text
test_user의 LAST_LOGIN_AT 수정
-> standard.testUser.TestUserService
test_user + test_user_token을 JOIN한 관리자 로그인 조회
-> bespoke.testUser.TestUserAuthService
```
`standard`는 테이블마다 Service를 하나만 둔다. `test_user`에 조회가 더 필요하면 `TestUserService`에 메서드와 DTO/VO를 추가한다. `TestUserLoginService`처럼 단일 테이블용 Service를 새로 만들지 않는다.
## 3. 패키지 모양
실행 모듈은 화면 메뉴의 1 depth를 패키지로 두고, 그 안에서 Controller와 업무 Service를 기능별로 나눈다.
```text
com.alist.api.admin.modules.backOffice
├─ AdminMemberController.java
├─ AccountController.java
├─ MenuController.java
├─ service
│ ├─ AdminMemberService.java
│ ├─ AccountService.java
│ └─ MenuService.java
├─ form
│ ├─ AdminMemberAddForm.java
│ ├─ AccountAddForm.java
│ └─ MenuAddForm.java
└─ vo
├─ AdminMemberListVo.java
├─ AccountListVo.java
└─ MenuListVo.java
```
패키지는 프로젝트 규칙에 따라 lower camel case로 작성한다. 따라서 `backOffice`가 패키지명이고, `AdminMemberController``MenuController`가 같은 업무 모듈의 기능별 진입점이다.
### Core bespoke 이름
core `bespoke`의 Service/Mapper는 기준 테이블명과 이를 호출하는 admin/front 업무 Service명을 결합한다.
```text
AdminMemberService
-> TestUserAdminMemberService
-> TestUserAdminMemberMapper
AuthService
-> TestUserAuthService
-> TestUserAuthMapper
```
`backOffice`는 실행 모듈의 메뉴 패키지 경계이므로 core bespoke 클래스명에 넣지 않는다. 이 기준으로 `backOffice.AdminMemberService`가 호출하는 `TEST_USER` JOIN 조회는 `TestUserBackOfficeService`가 아니라 `TestUserAdminMemberService`에 둔다.
### core standard
```text
com.alist.api.core.modules.standard.testUser
├─ dto
│ ├─ TestUserAddDto
│ ├─ TestUserListDto
│ ├─ TestUserModifyDto
│ └─ TestUserViewDto
├─ vo
│ ├─ TestUserAddVo
│ ├─ TestUserListItemVo
│ ├─ TestUserListVo
│ ├─ TestUserModifyVo
│ └─ TestUserViewVo
├─ mapper
│ └─ TestUserMapper
└─ service
└─ TestUserService
resources/mapper/standard/testUser
└─ TestUserMapper.xml
```
### core bespoke
```text
com.alist.api.core.modules.bespoke.testUserToken
├─ dto
│ └─ TestUserTokenRefreshViewDto
├─ vo
│ └─ TestUserTokenRefreshViewVo
├─ mapper
│ └─ TestUserTokenAuthMapper
└─ service
└─ TestUserTokenAuthService
resources/mapper/bespoke/testUserToken
└─ TestUserTokenAuthMapper.xml
```
### admin/front 업무 모듈
```text
com.alist.api.admin.modules.auth
├─ AuthController
├─ form
│ └─ AuthLoginForm
├─ service
│ └─ AuthService
└─ vo
└─ AuthLoginVo
```
front도 같은 모양을 사용한다. 단, Controller와 Form/VO는 서로 공유하지 않는다.
## 4. 클래스별 역할
| 클래스 | 위치 | 하는 일 |
|---|---|---|
| `*Controller` | admin/front | HTTP 요청 수신, Form 검증, 인증 주체 확인, `ApiResponse` 반환 |
| admin/front `*Service` | admin/front | 업무 정책, 여러 core 호출 조합, 트랜잭션, core VO를 외부 VO로 변환 |
| `*Service` | core standard | 한 테이블의 데이터 접근 계약 제공 |
| `*Service` | core bespoke | JOIN·집계·복합 SQL 데이터 접근 계약 제공 |
| `*Mapper` | core | Java 메서드와 Mapper XML 연결 |
| `*Mapper.xml` | core resources | 실제 SQL |
| `*Form` | admin/front | HTTP 요청 바인딩과 입력 검증 |
| `*Dto` | core | Service에서 Mapper로 전달하는 조건 또는 저장 값 |
| core `*Vo` | core | Mapper 조회 결과와 core 내부 처리 값 |
| admin/front `*Vo` | admin/front | 외부 API 응답 값 |
Controller는 core Service, Mapper, core DTO/VO를 직접 사용하지 않는다. admin/front 업무 Service가 경계를 지킨다.
## 5. 이름 읽는 법
### 패키지명
패키지는 lower camel case를 쓴다.
```text
testUser
testUserToken
testCorsAllowedList
```
### 클래스명
```text
standard
{Table}Service, {Table}Mapper
예: TestUserService, NoticeMapper
bespoke
{BaseTable}{BusinessModule}Service, {BaseTable}{BusinessModule}Mapper
예: TestUserAuthService, NoticeSupportService
```
`admin`, `front`, `Query`, `Custom`은 core bespoke Service 이름에 붙이지 않는다. core는 서버가 아니라 데이터와 업무 목적을 기준으로 재사용하기 때문이다.
### 메서드명
```text
admin/front 업무 Service
{select|insert|update|delete}{기능명}
예: selectLogin, selectRefresh, updateLogout
core standard
{select|insert|update|delete}{Table}{Action}
예: selectTestUserList, updateTestUserModify
core bespoke
{baseTable}{function}{ResultShape}
예: testUserLoginView, testUserTokenRefreshView, noticeSupportList
```
bespoke 조회 메서드는 결과 모양을 마지막에 붙인다. `List`, `View`, `Count`, `Summary`, `Tree` 등이 예다. 조회 조건은 `ByUserIdx`처럼 이름에 붙이지 않고 DTO에 둔다.
### 변수명
변수는 타입과 역할이 드러나게 쓴다.
```java
AuthLoginVo authLoginVo;
TestUserTokenModifyDto testUserTokenModifyDto;
List<NoticeListItemVo> noticeList;
```
`result`, `data`, `dto`, `vo`처럼 의미가 너무 넓은 이름은 피한다. boolean은 결과가 드러나게 `added`, `updated`, `deleted`, `loggedIn`, `refreshed`처럼 작성한다.
## 6. 인증 흐름 예시
관리자 로그인은 JOIN 조회와 단일 테이블 수정을 함께 사용한다.
```text
AuthController.login
-> AuthService.selectLogin
-> TestUserAuthService.testUserLoginView
-> test_user + test_user_token JOIN
-> PasswordEncoder로 비밀번호 검증
-> JwtTokenProvider로 Access/Refresh Token 발급
-> TestUserTokenService.updateTestUserTokenModify
-> test_user_token refresh token, 만료 시각 저장
-> TestUserService.updateTestUserModify
-> test_user LAST_LOGIN_AT 저장
-> AuthLoginVo 조립
-> ApiResponse<AuthLoginVo> 반환
```
이 흐름에서 `password`, `refreshToken`은 인증 처리용 core VO에만 두고 외부 응답 VO에는 기본적으로 넣지 않는다.
## 7. 새 기능을 만들 때 순서
1. API가 admin인지 front인지 정하고 해당 `modules/{기능}` 패키지를 만든다.
2. 사용할 SQL이 단일 테이블인지, JOIN·복합 SQL인지 판단한다.
3. core standard 또는 bespoke에 DTO, VO, Mapper, Service, Mapper XML을 만든다.
4. admin/front에 Form, 업무 Service, 외부 응답 VO, Controller를 만든다.
5. Controller가 core 타입을 직접 import하지 않는지 확인한다.
6. Swagger `@Tag`, `@Operation`, `@Schema`과 컴파일 검증을 한다.
상세 규칙은 [Core 모듈 설계](core-module-design.md), [코드 컨벤션](code-conventions.md), [보안 및 응답 규칙](security-and-response.md)을 참고한다.
+60 -17
View File
@@ -1,35 +1,65 @@
# 코드 컨벤션
## 파일 생성 규칙
- 새 기능은 가능하면 `modules/{도메인명}Controller` 단위로 패키지를 만들고 그 아래에 `service`, `mapper`, `dto`, `vo`를 필요한 만큼만 추가한다.
- core 업무 모듈은 `modules.standard``modules.bespoke` 기준으로 나눈다. standard는 단일 테이블의 모든 조회·등록·수정·삭제와 필요한 전용 조회를 담당하고, bespoke는 여러 테이블 JOIN·집계·복합 SQL을 위한 전용 데이터 처리다.
- 업무 모듈 패키지명은 단어를 이어 쓰는 lower camel case로 작성한다. 예: `testUser`, `testUserToken`, `testCorsAllowedList`.
- admin/front의 새 API는 `modules/{업무 메뉴 1 depth}` 단위로 Controller, 업무 Service, Form, 화면 VO를 둔다. 이 1 depth는 테이블명이 아니라 서비스 메뉴와 업무 경계 기준이다. 예: 백오피스의 관리자 계정, 권한, 메뉴 관리는 모두 `modules.backOffice`에 둔다.
- 하나의 업무 메뉴 모듈에는 여러 Controller와 업무 Service가 들어갈 수 있다. 예: `backOffice`에는 `AdminMemberController`/`AdminMemberService`, `AccountController`/`AccountService`, `MenuController`/`MenuService`를 함께 둔다.
- admin/front의 패키지명은 lower camel case로 작성한다. 예: `modules.backOffice`. 업무 모듈이 이미 기능 경계이므로 Form과 외부 VO는 기본적으로 해당 모듈의 `form`, `vo` 바로 아래에 둔다. 예: `modules.backOffice.form.AdminMemberAddForm`, `modules.backOffice.vo.AdminMemberListVo`. 클래스명은 `AdminMemberController`, `AdminMemberService`처럼 UpperCamelCase를 사용한다.
- core에는 Controller, Form, 화면 전용 VO를 두지 않는다. 다만 core Service/Mapper의 업무 입력과 결과를 위한 내부 DTO/VO는 둔다.
- core standard Service는 `{Table}Service`로 작성한다. 예: `TestUserService`, `NoticeService`.
- core bespoke Service는 `{BaseTable}{AdminFrontServiceName}Service`로 작성한다. 예: `TestUserAuthService`, `TestUserAdminMemberService`, `NoticeNoticeService`.
- `{AdminFrontServiceName}`은 admin/front 업무 Service의 이름에서 마지막 `Service`를 뺀 값이다. 기준 테이블명과 같은 이름이 반복되어도 생략하거나 상위 패키지명으로 치환하지 않는다. 예: `modules.support.NoticeService`가 호출하는 core bespoke는 `NoticeSupportService`가 아니라 `NoticeNoticeService`로 작성한다.
- core bespoke 명명에 패키지명이나 1 depth 모듈명은 사용하지 않는다. 패키지 위치는 구조 변경으로 달라질 수 있으므로, 변경에 강한 기준 테이블명과 admin/front 업무 Service명을 조합한다.
- 따라서 `backOffice`는 admin 실행 모듈의 패키지 경계일 뿐 core bespoke 명명에 사용하지 않는다. `backOffice.AdminMemberService`의 core bespoke는 `TestUserBackOfficeService`가 아니라 `TestUserAdminMemberService`로 작성한다.
- `admin`, `front` 같은 실행 서버명과 `Query`, `Custom`, `Bespoke` 같은 구현 방식명은 core bespoke 이름에 사용하지 않는다.
- 하나의 실행 모듈에 여러 테이블 기능이 있으면 Service도 테이블별로 나눈다. 예: `support` 모듈의 `NoticeSupportService`, `FaqSupportService`. `SupportService` 하나에 서로 다른 테이블 업무를 모으지 않는다.
- core bespoke Service/Mapper 메서드는 `{BaseTable}{Function}{List|View|Count|Summary|...}`로 작성한다. 예: `testUserLoginView`, `testUserTokenRefreshView`, `noticeSupportList`.
- bespoke 메서드의 `{Function}`은 호출하는 admin/front Service의 기능명을 따르며, core는 앞에 기준 테이블명을 붙인다. 예: `AuthService.selectLogin``testUserLoginView`, `testUserTokenLoginView`를 호출할 수 있다.
- bespoke 메서드에는 `select`, `insert`, `update`, `delete` 접두어와 `ByUserIdx` 같은 조회 조건을 넣지 않는다. 조건은 DTO에 둔다.
- 요청 검증이나 JSON 바인딩이 필요하면 `form` 패키지를 함께 만든다.
- 클래스명은 역할이 바로 드러나게 `도메인명 + 역할` 형식을 유지한다. 예: `ApiService`, `ApiMapper`, `ApiVo`
- Mapper 인터페이스를 추가하면 같은 이름의 XML을 `src/main/resources/mapper/{도메인경로}` 아래 함께 만든다.
- 단순 예시 코드와 운영 코드는 섞지 말고 패키지로 분리한다.
- 설정성 클래스는 `config` 하위 역할별 패키지에 둔다. 예: `config.jwt`, `config.exception`, `config.properties`
- 설정성 클래스는 `config` 하위 역할별 패키지에 둔다. 예: `config.datasource`, `config.cache`.
- 단순 환경값(String, 숫자, boolean 등)은 사용하는 클래스의 필드에 `@Value`로 주입한다. 예: `@Value("${file.storage.root-path}") private String rootPath;`.
- 목록, Map, 중첩 객체처럼 구조화된 설정은 `@ConfigurationProperties`로 묶는다. 단순 값 몇 개만을 위해 별도 Properties 클래스를 만들지 않는다.
## DTO/VO/Form 규칙
- `Dto`는 각 Controller 에서 Service, Mapper 로 전달하는 파라미터 성격의 값 객체로 정의한다.
- core DTO는 Mapper와 core Service 사이의 내부 데이터 전달 용도다. admin/front Form과 화면 전용 VO를 재사용하지 않는다.
- `Dto`는 core Service와 Mapper 사이의 파라미터 성격 값 객체로 정의한다.
- `Dto`는 요청 처리에 필요한 저장, 수정, 로그 적재 같은 작업 파라미터를 담는 용도로 우선 사용한다.
- `Vo`는 Mapper 에서 Service, Controller 로 반환하는 값 객체로 정의한다.
- `Vo`는 Mapper 조회 결과를 담을 수 있고, Service 에서 비즈니스 로직 처리 후 필요한 데이터를 가공해서 Controller 로 반환하는 용도로 사용한다.
- `Form`은 Controller 입력 검증과 요청 바인딩 전용으로 두고, `@Valid` 와 Jakarta Validation 어노테이션을 우선 사용한다.
- Form/Dto/Vo 클래스명은 가능한 한 Controller 메서드명을 기준으로 맞춘다. 예: Controller 메서드가 `adminMemberList` 이면 `AdminMemberListForm`, `AdminMemberListDto`, `AdminMemberListVo` 를 사용한다.
- core VO는 Mapper 조회 결과와 core 내부 업무 처리를 위한 값 객체다. 화면/API 응답 전용 VO가 아니다.
- admin/front VO는 외부 HTTP 응답 전용이며, 실행 모듈 업무 Service가 core VO에서 필요한 필드만 골라 조립한다.
- 동일한 core VO를 사용하더라도 admin/front는 목적에 따라 서로 다른 외부 VO를 사용한다.
- Controller는 core DTO/VO를 import하지 않고, core DTO/VO를 직접 반환하거나 외부 VO로 변환하지 않는다. 변환은 admin/front 업무 Service에서 끝낸다.
- core VO를 admin/front 외부 VO로 바꾸는 단순 필드 복사는 별도 `Converter`, `Assembler`, 변환 전용 private 메서드로 분리하지 않는다. 해당 업무 Service 메서드 안에서 외부 VO를 생성하고 필요한 필드를 직접 설정한다.
- 목록 변환은 업무 Service 메서드 안에서 `for`문으로 목록 행 VO를 생성해 결과 목록에 추가한다. 변환 규칙이 여러 Service에서 반복되거나 독립된 정책을 가지는 시점에만 별도 변환 클래스를 검토한다.
- Controller의 Service 메서드 반환 타입은 원칙적으로 해당 실행 모듈의 VO 또는 단순 처리 결과여야 한다. 예: `AuthService.loginChecked(...)``AdminAuthUserVo`가 아닌 `AuthLoginCheckedVo`를 반환한다.
- 비밀번호, refresh token, 내부 인증값은 일반 core VO와 admin/front 외부 VO에 기본으로 포함하지 않는다. 인증 처리에서만 필요한 값은 별도 내부 VO로 제한한다.
- `Form`은 실행 모듈의 요청 바인딩과 입력 검증에 사용하고, `@Valid` 와 Jakarta Validation 어노테이션을 우선 사용한다. 실행 모듈의 업무 Service가 Form을 받아 core DTO로 변환할 수 있지만, core는 Form에 의존하지 않는다.
- Form은 실행 모듈의 Controller 메서드명을 기준으로, core DTO/VO는 업무 모듈과 동작명을 기준으로 이름을 맞춘다. 예: `NoticeAddForm` -> `NoticeAddDto`, `NoticeListDto` -> `NoticeListVo`.
- standard 업무 모듈의 기본 DTO/VO 동작명은 `Add`, `View`, `List`, `Modify`, `Delete`다. 목록 행은 `{Domain}ListItemVo`를 사용한다.
- 단일 상태 변경도 기본적으로 별도 DTO를 만들지 않고 `{Domain}ModifyDto`의 선택 필드로 처리한다. Mapper XML은 `<set>` 없이 작성하고, 항상 갱신하는 수정일 컬럼 뒤에 `<if>`로 값이 전달된 필드만 추가 수정한다.
- 신규 API 에서는 path 동작명과 다른 표현을 지양한다. 예: 목록은 `Search` 보다 `List`, 상세는 `Detail` 보다 `View`, 수정은 `Update` 보다 `Modify` 를 우선 사용한다.
- 현재 코드처럼 DTO/VO는 Lombok `@Getter`, 필요한 경우에만 `@Setter`를 사용한다.
- Form 안에는 DTO 변환 메서드를 둘 수 있으며, 짝이 되는 Dto 로 변환하는 메서드는 `toDto()` 기본으로 사용한다. 예: `AdminMemberListForm#toDto()``AdminMemberListDto` 를 반환한다.
- 하나의 Form 이 여러 Dto 로 변환되어야 하는 예외 상황에서만 `toAdminMemberListDto()` 처럼 대상 Dto 이름을 명시한다.
- Form 에서 DTO 로 변환할 때는 검증과 `trim()`, 기본값 치환까지 같이 처리하는 현재 패턴을 우선 따른다.
- Form 안에는 단순한 값 정리 메서드를 둘 수 있다. core DTO 변환은 기본으로 실행 모듈 업무 Service가 담당한다.
- Form 에서 전달되는 문자열은 업무 Service에서 core DTO로 변환할 때 `trim()`과 기본값 치환을 적용한다.
- DTO 안에 연관된 다른 DTO 변환이 꼭 필요할 때만 최소한의 보조 메서드를 둔다.
- 외부 응답에 노출되면 안 되는 내부 필드는 응답에 사용될 수 있는 객체에서 `@JsonIgnore`로 숨긴다.
- 등록, 수정, 삭제, 상태 변경처럼 처리 여부만 반환하는 VO 의 boolean 필드는 `processed` 로 통일한다. 예: `private boolean processed;`
- 변경 결과 VO의 boolean 필드는 동작이 드러나도록 작성한다. 예: 등록은 `added`, 수정/상태 변경은 `updated`, 논리 삭제는 `deleted`.
- `ModifyVo`, `DeleteVo` 등 변경 결과 VO에는 처리 대상 PK도 함께 둔다. 예: `NoticeModifyVo``updated`, `noticeIdx`; `NoticeDeleteVo``deleted`, `noticeIdx`.
- 변경 결과 VO에는 내부 처리용 `resultCode`를 두고 Service가 성공/실패 코드를 설정한다. `resultCode``@JsonIgnore`로 숨기고, Controller가 이를 기준으로 `ApiResponseCode`를 선택한다.
- Controller 요청/응답 타입은 가능한 한 Form/Dto/Vo 로 명시한다. `Map<String, Object>` 는 응답 구조가 고정되지 않는 임시 디버깅, 외부 라이브러리 passthrough 같은 예외 상황에서만 제한적으로 사용한다.
- 응답 필드가 단순 boolean 몇 개뿐이어도 운영 API 에서는 VO 를 만든다. 예: 로그인 상태 확인 응답은 `Map<String, Object>` 보다 `AdminAuthLoginCheckedVo` 를 사용한다.
- 목록 조회 API 는 특별한 사유가 없으면 기본적으로 페이징을 적용하고, 목록 조회 Form/Dto 는 `PageRequest`, 목록 응답 Vo 는 `PageResponse` 상속한다.
- 목록 조회 API 는 특별한 사유가 없으면 기본적으로 페이징을 적용한다. core 내부 페이징 기준은 `PagingRequest`, `PagingResponse`사용한다.
## Swagger/OpenAPI 문서화 규칙
- 신규 또는 수정되는 Controller, Form, VO에는 Swagger 테스트 편의성을 위한 설명을 반드시 남긴다.
- Controller 클래스에는 `@Tag`를 사용해 API 그룹명과 설명을 작성한다.
- `@Tag`의 번호는 Controller 생성 순서가 아니라 admin/front의 업무 메뉴 1 depth 기준 알파벳 그룹으로 관리한다. 예: `backOffice``B` 그룹이며 그 안의 Controller는 `B-1. 관리자 계정 관리`, `B-2. SSO 클라이언트 관리`, `B-3. 스케줄 관리`처럼 작성한다. 같은 1 depth 안에서는 기능 추가 순서에 따라 하위 번호를 증가시킨다.
- Controller 메서드에는 `@Operation`으로 `summary``description`을 작성한다.
- `@RequestBody` Form 클래스에는 클래스 레벨 `@Schema(description = "...")`를 작성한다.
- `@ModelAttribute` 검색 Form 필드에도 `@Schema`를 작성해 Swagger query parameter 설명과 예시가 보이게 한다.
@@ -45,8 +75,11 @@
- 페이징 응답의 `rowStartNum`처럼 계산 방식이 필요한 값은 프론트 사용 방법까지 `description`에 남긴다.
## Mapper/MyBatis 규칙
- Service/Mapper 메서드명은 주된 DB 동작이 드러나도록 `select`, `insert`, `update` 접두어를 먼저 붙이고 Controller 메서드명을 이어서 만든다. 예: Controller 메서드가 `adminSsoClientList` 이면 Service/Mapper 조회 메서드`selectAdminSsoClientList`.
- 삭제 API도 기본적으로 물리 삭제가 아니라 상태 변경으로 처리하므로 실제 SQL 동작에 맞춰 `update...Delete` 형태를 우선 사용한다. 예: Controller 메서드가 `adminSsoClientDelete` 이면 Service/Mapper 메서드는 `updateAdminSsoClientDelete`.
- standard Mapper에는 단일 테이블에서 재사용할 수 있는 표준 CRUD와 전용 조회를 둔다. 조회 조건 또는 조회 projection이 추가되어도 해당 테이블의 기존 standard Service/Mapper 메서드를 추가한다.
- bespoke Mapper에는 업무 전용 조인 조회를 우선 둔다. 여러 테이블을 함께 갱신해야 하는 `JOIN UPDATE`, 일괄 처리, `INSERT ... SELECT`, 성능 최적화 SQL은 예외적으로 둘 수 있다. bespoke Mapper는 core bespoke Service를 통해 호출한다.
- standard Service/Mapper 메서드는 `{select|insert|update|delete}{Table}{Action}`으로 작성한다. 예: `selectTestUserList`, `insertTestUserAdd`, `updateTestUserModify`.
- bespoke Service/Mapper 메서드는 앞 동작 접두어 없이 `{BaseTable}{Function}{ResultShape}`로 작성한다. 예: `testUserLoginView`, `noticeSupportList`. XML `id`도 동일하게 맞춘다.
- 삭제 API도 기본적으로 물리 삭제가 아니라 상태 변경으로 처리하므로 standard는 `update...Delete` 형태를 우선 사용한다.
- 물리 삭제가 정책상 명확히 필요한 예외 상황에서만 `delete` SQL과 `delete...` 메서드명을 사용한다.
- SQL 을 여러 줄로 작성할 때 콤마는 다음 줄 앞에 둔다. 예: `SELECT COL1`, 다음 줄 `, COL2`.
- Mapper XML `namespace`는 인터페이스의 전체 경로와 정확히 일치시킨다.
@@ -57,26 +90,36 @@
- `map-underscore-to-camel-case: true` 설정으로 DB 컬럼 `user_idx` 는 Java 필드 `userIdx` 로 자동 매핑된다.
- `useGeneratedKeys`, `keyProperty` 를 사용하는 insert 가 있으므로 신규 PK 생성 테이블은 현재 패턴을 먼저 확인한다.
- 상태 집계나 이력성 데이터는 단건 update 외에 이벤트 로그 insert 가 같이 필요한지 확인한다.
- 동적 수정 SQL에서 `<set>`은 사용하지 않는다. `SET UPDATE_AT = NOW()`처럼 항상 갱신하는 컬럼을 먼저 두고, 선택 필드는 `, COLUMN = #{value}` 형태의 `<if>`로 이어 붙인다. null은 기본적으로 "수정하지 않음"을 뜻한다. null로 컬럼 값을 비워야 하는 요구는 별도 플래그 또는 업무 전용 SQL로 명확히 구분한다.
## Java/Spring 코드 스타일
- Java 코드는 현재 프로젝트 스타일에 맞춰 탭/들여쓰기와 import 정렬을 유지한다.
- 선언문, 메서드 반환 대입, 단순 setter, 삼항식이 한 번에 읽히면 줄 길이만으로 줄바꿈하지 않고 한 줄로 작성한다. 여러 줄 정렬은 한 줄에 담기 어려운 인자, 조건, SQL처럼 가독성이 실제로 좋아지는 경우에만 사용한다.
- Mapper 인터페이스의 메서드 선언은 한 줄로 작성하고, 메서드 선언 사이에는 한 줄을 띄운다.
- `if`, `for`, `while` 조건문은 한 줄이어도 항상 중괄호 블록을 사용한다. Service 호출 결과를 지역 변수에 담은 뒤 후속 조건문을 시작하기 전에는 한 줄을 띄워 흐름을 구분한다.
- 코드는 초급 개발자가 읽어도 흐름을 따라갈 수 있도록 직관적으로 작성한다. 과한 축약, 기교적인 표현, 불필요하게 복잡한 체이닝보다 명확한 변수명과 단계적인 흐름을 우선한다.
- Java 코드에서 메서드 호출, 어노테이션 인자, 생성자 인자 등을 여러 줄로 작성할 때 콤마는 다음 줄 앞에 둔다. 예: `summary = "회원 목록 조회"` 다음 줄 `, description = "..."`
- 기능 수정/추가 시 가능하면 Red-Green-Refactor 흐름을 따른다. 먼저 실패하는 테스트나 재현 가능한 검증 조건을 만들고(Red), 최소 구현으로 통과시키며(Green), 이후 컨벤션과 가독성에 맞게 정리한다(Refactor). 테스트 작성이 어려운 경우에는 최소 실행 검증 절차를 먼저 정하고 결과를 보고한다.
- Lombok은 반복 보일러플레이트 제거에만 절제해서 사용한다.
- Lombok은 반복 보일러플레이트 제거에만 절제해서 사용한다. `@RequiredArgsConstructor`는 사용하지 않고, 의존성 주입은 명시적인 생성자로 작성한다.
- 로그는 `Slf4j`를 사용하고 반복문 내부 대량 출력은 지양한다.
- 새로운 기능은 가능하면 역할이 드러나는 패키지로 분리한다.
- 생성자 주입을 기본으로 하고 필드 주입은 추가하지 않는다.
- Service, Config 등 다른 Spring Bean 의존성은 생성자 주입을 기본으로 하고, `@RequiredArgsConstructor` 대신 명시적인 생성자를 사용한다. 단, 단순 환경값은 위 설정 규칙에 따라 `@Value` 필드 주입을 사용한다.
- Service 에서 DB 상태를 바꾸는 메서드는 필요한 범위에서 `@Transactional`을 사용하고, 조회 전용은 `readOnly = true`를 우선 검토한다.
- 문자열 입력값은 현재 코드처럼 필요한 지점에서 `trim()` 처리하고, null 가능성 여부를 먼저 확인한다.
- 지역 변수명은 `result`, `list`, `vo`, `dto` 처럼 범용적인 이름보다 도메인과 객체 종류가 드러나게 작성한다. 예: `List<AdminMemberVo> adminMemberList`, `AdminMemberListVo adminMemberListVo`, `AdminMemberListDto adminMemberListDto`.
- Service 메서드의 Form, DTO, VO 파라미터와 지역 변수도 범용명 `form`, `dto`, `vo`를 사용하지 않는다. 예: `AdminMemberListForm adminMemberListForm`, `SsoListForm ssoListForm`, `TestUserAdminMemberListDto testUserAdminMemberListDto`.
- 같은 타입 또는 같은 의미의 변수가 여러 개 필요해도 객체 종류를 유지하고, 구분이 필요한 경우 숫자 suffix 를 제한적으로 사용한다. 예: `AdminMemberVo adminMemberVo1`, `AdminMemberVo adminMemberVo2`.
## Controller/Service 규칙
- Controller 는 요청/응답 조립과 인증 주체 확인에 집중하고, DB 처리나 복잡한 계산은 Service 로 넘긴다.
- Controller 는 요청/응답 조립과 인증 주체 확인에 집중하고, core Service 또는 Mapper를 직접 호출하지 않는다. core DTO/VO도 Controller에 노출하지 않는다.
- admin/front 업무 Service가 Form을 받아 업무 정책, standard/bespoke 호출 순서, 트랜잭션 경계를 담당한다.
- admin/front 업무 Service 메서드는 `{select|insert|update|delete}{Controller 기능명}`으로 작성한다. 예: Controller의 `ssoList()``selectSsoList()`, `ssoAdd()``insertSsoAdd()`, `ssoDelete()``updateSsoDelete()`를 호출한다. 논리 삭제는 실제 DB 작업이 수정이므로 `update`를 사용한다.
- admin/front 업무 Service의 public 메서드는 Controller가 호출한 API 한 건의 흐름을 가능한 한 해당 메서드 안에서 완결한다. Form 값 정리, core Service 호출, 결과 VO 조립을 별도 모듈 보조 메서드로 잘게 나누지 않는다.
- `to...`, `create...`, `select...` 같은 private 보조 메서드는 단순 코드 분리를 위해 먼저 만들지 않는다. 같은 로직이 여러 업무 Service에 반복되거나, 독립적인 정책과 복잡도를 가질 때 리팩터링 대상으로 분리한다.
- core bespoke Service는 bespoke Mapper를 통한 JOIN·집계·복합 SQL 데이터 접근을 담당한다. 단일 테이블의 조회·등록·수정·삭제는 core standard Service와 Mapper에 둔다.
- Controller 응답은 `ResponseEntity<ApiResponse<T>>` 를 기본으로 사용한다. 파일 다운로드처럼 바이너리 응답이 필요한 경우만 예외로 둔다.
- Controller 에서는 `@RequestBody`, `@PathVariable`, `@RequestHeader`, `@CookieValue` 를 명시적으로 선언해 요청 출처를 드러낸다.
- 인증 사용자 확인은 모듈 구현에 따라 `HttpSession` 또는 `SecurityContextHolder` 를 사용하므로 기존 방식을 먼저 맞춘다.
- 목록 조회는 Controller 에서 `@ModelAttribute` 검색 Form 을 받고, Service 에서 `totalCount` 와 목록을 조회한 뒤 `PageResponse#setPaging(...)` 으로 페이징 정보를 채운다.
- 목록 조회는 Controller 에서 `@ModelAttribute` 검색 Form 을 받고, Service 에서 `totalCount` 와 목록을 조회한 뒤 `PagingResponse#setPaging(...)` 으로 페이징 정보를 채운다.
- 신규 API path 의 동작명은 CRUD 성격에 맞춰 통일한다. 목록은 `list`, 상세 조회는 `view`, 등록은 `add`, 수정은 `modify`, 삭제는 `delete` 를 사용한다.
- Controller 메서드명은 가능한 한 path 조합을 기준으로 만든다. 클래스 레벨 path 와 메서드 레벨 path 를 이어 붙인 의미가 드러나게 작성한다. 예: 클래스 path 가 `/admin/member`, 메서드 path 가 `/list` 이면 `adminMemberList`, 클래스 path 가 `/admin/sso/client`, 메서드 path `/view` 이면 `adminSsoClientView`.
- Controller 메서드명은 path 동작과 기능을 그대로 표현한다. 예: `ssoList`, `ssoView`, `ssoAdd`, `ssoModify`, `ssoDelete`, `adminMemberDelete`. HTTP path와 Controller 메서드는 API 기능을 나타내고, 실제 DB 작업 성격은 Controller가 호출하는 Service 메서드명에서 표현한다. admin/front 분리 서버에서는 path `/admin` 또는 `/front` 접두어를 붙이지 않는다.
+41 -13
View File
@@ -1,15 +1,43 @@
# 현재 코드베이스 메모
## 현재 특히 주의할 점
- `README.md``docs/*.md`는 코드 변경 후 같이 갱신해야 한다. 의심되는 항목은 Controller, `SecurityConfig`, 설정 파일을 기준으로 다시 확인한다.
- 예시 스케줄러 코드에는 깨진 문자열과 과도한 반복 로그가 보일 수 있으니 관련 수정 시 인코딩과 로그 정책을 함께 점검한다.
- 테스트 코드가 충분하지 않을 수 있으므로 기능 변경 시 필요한 테스트를 보강한다.
- 일부 Java 소스와 주석, Swagger 설명에 인코딩이 깨진 문자열이 있으므로 표시 문자열 수정은 영향 범위를 보고 묶어서 처리한다.
- `application-local.yaml` 은 로컬 실행값이 직접 들어가 있으므로 공유하거나 커밋할 때 민감정보 노출 여부를 한 번 더 확인한다.
- 파일 업로드는 두 흐름으로 분리되어 있다. `file` 모듈의 단순 업로드는 DB에 기록하지 않고 `uploadPath`, 파일명, 확장자, contentType, size, image width/height 정도만 반환한다. 각 업무 테이블 저장은 호출 측에서 처리한다.
- SunEditor 이미지 업로드는 API로 파일을 저장하되 응답 URL은 `file.upload.view.file-domain + uploadPath` 로 만든다. 에디터 본문에 저장되는 URL은 admin/user 토큰에 의존하지 않는 file-domain URL이어야 한다.
- TUS 업로드는 `tusFile` 모듈에서 DB master/detail 기록, upload token 발급, tusd hook 반영, 상태 조회, view/download/delete를 담당한다.
- TUS 인증 경로는 `/tus/file/upload/auth`, hook 경로는 `/tus/file/hook` 이다. nginx `/_upload_auth`, Spring Security 공개 경로, `tus-file.upload.tus-endpoint` 를 함께 맞춘다.
- 단순 파일 API는 `/file/upload`, `/file/suneditor/upload`, `/file/view`, `/file/download` 를 사용한다. `/admin/files/...` 또는 `/files/...` 예전 표기가 남아 있으면 최신 경로로 정리한다.
- file-domain nginx 는 `/uploads/` 를 공개 정적 파일로 열고 `/uploads/tmp/` 는 404로 막는다. TUS 임시 디렉터리는 파일 시스템에서는 사용하지만 URL로 직접 노출하지 않는다.
- Admin 로그인 상태 확인은 `/admin/auth/loginChecked` 를 사용한다. `/admin/auth/refresh` 는 토큰을 재발급하므로 새로고침 상태 확인용으로 쓰지 않다.
## 멀티모듈 이관 상태
- 기존 단일 모듈의 업무 API 이관을 완료하고, 레거시 소스는 저장소에서 제거했습니다.
- core/admin/front 모듈은 각각 독립 bootJar로 빌드·배포합니다.
- 이후 신규 기능도 core standard/bespoke와 admin/front 업무 Service 경계를 기준으로 추가합니다.
## 이관 원칙
- 범용적인 단일 테이블 데이터 처리는 `core.modules.standard`에 둡니다.
- JOIN·집계·복합 SQL 데이터 처리는 `core.modules.bespoke`에 둡니다.
- admin/front는 Controller, 업무 Service, Form, 화면 VO를 소유합니다.
- admin/front 업무 Service가 업무 정책과 트랜잭션을 소유하며 core standard/bespoke Service를 조합합니다.
- Mapper는 core standard/bespoke에 통합하고, Controller는 core Service 또는 Mapper를 직접 호출하지 않습니다.
- core VO는 내부 업무·조회 결과를 표현하고, admin/front 업무 Service가 필요한 필드만 화면/API 응답 VO로 조립합니다.
- 비밀번호, refresh token 등 민감 정보는 일반 core VO와 외부 응답 VO에 포함하지 않습니다.
- core는 화면 전용 VO나 Controller를 소유하지 않습니다.
## 파일과 TUS
- 단순 파일 업로드는 DB 기록 없이 파일을 저장하고 uploadPath를 반환합니다.
- 일반 file Controller는 admin/front 양쪽에 둘 수 있으며, 저장 구현은 core 공통 기능으로 이관합니다.
- TUS는 `02-api-admin/modules/tus`의 admin 전용 기능입니다. front에는 TUS Controller나 `tus-file.*` 설정을 두지 않습니다.
- TUS Controller는 요청 바인딩과 응답만 담당하고, `TusFileService`가 core standard/bespoke Service를 조합합니다.
- `FILE_MASTER`, `FILE_DETAIL`, 업로드·다운로드 이벤트 로그의 단일 테이블 처리는 core standard를 사용합니다. 토큰 소유권, 완료 파일 목록·조회, 파일 이동 대상 조회, 마스터 집계는 core bespoke `fileDetailTusFile`, `fileMasterTusFile`에 둡니다.
- 완료 파일의 최종 저장 루트는 일반 업로드와 같은 `file.storage.root-path`입니다. TUS 임시 파일은 admin의 `tus-file.upload.tmp-root`를 사용합니다.
- TUS 업로드 인증 경로와 Hook 경로만 admin SecurityConfig에서 비인증으로 열고, 나머지 TUS API는 ADMIN 권한을 요구합니다.
- tusd 실제 Hook, 임시 파일 이동, nginx 파일 제공은 tusd와 file-domain 설정이 준비된 환경에서 통합 검증합니다.
## CORS 동기화
- Dynamic CORS 필터와 캐시는 core를 통해 admin/front 양쪽 JVM에서 동작합니다.
- CORS 관리 API는 admin에만 둡니다.
- 허용 Origin 목록은 Redis `alist:cors:allow-origins` 키에 JSON 목록으로 저장합니다.
- admin에서 정책을 변경하면 트랜잭션 커밋 후 Redis Pub/Sub 채널 `alist:cors:allow-origins:changed`로 갱신 이벤트를 발행합니다.
- 각 JVM은 해당 채널을 구독해 즉시 Redis 목록을 다시 읽고, 예외 상황에 대비해 60초 주기 Redis 재조회도 수행합니다.
## 주의 사항
- `application-local.yaml`에 민감정보가 있을 수 있으므로 공유와 커밋 전에 확인합니다.
- Mapper 인터페이스, XML namespace, XML id, DTO resultType/parameterType은 이관 시 함께 변경합니다.
- 파일 경로 변경은 nginx alias, tusd 경로, API 환경 설정을 함께 확인합니다.
+280
View File
@@ -0,0 +1,280 @@
# Core 모듈 설계
## 목표
- core는 admin/front가 공유하는 데이터 접근 계층과 인프라를 제공한다.
- admin/front는 각 서버의 업무 정책, 트랜잭션, 요청과 응답 조립, 인증 주체 확인, 서버별 운영 설정을 담당한다.
- 화면 전용 Form과 VO는 core에 두지 않는다. 단, core 업무 처리의 입력과 결과를 표현하는 내부 DTO/VO는 둔다.
## 패키지 구조
```text
com.alist.api.core
├─ common
├─ config
└─ modules
├─ standard
└─ bespoke
```
## standard
`standard`는 여러 기능에서 재사용할 수 있는 **단일 테이블** 데이터 처리를 둡니다.
- 단일 테이블의 조회, 등록, 수정, 삭제와 필요한 전용 조회 projection
- 한 테이블 중심의 Mapper, Service, 내부 DTO/VO
- admin/front 업무 Service가 호출한다.
- Form, 화면 전용 VO, HTTP 요청/응답 객체에 의존하지 않음
- 등록과 수정의 실제 SQL 처리는 특별한 사유가 없으면 standard가 소유한다.
- 단일 테이블에서 조회 조건이나 조회 필드가 추가되어도 새 Service를 만들지 않는다. 해당 테이블의 기존 standard Service, Mapper, DTO/VO 계약에 메서드 또는 선택 조건을 추가한다.
## bespoke
`bespoke`는 단일 테이블 standard 계약으로 해결되지 않는 복합 데이터 처리를 둡니다. bespoke는 특정 목적에 맞춰 제작한 처리라는 뜻입니다.
- 여러 테이블 JOIN 조회
- standard로 처리할 수 없는 일괄 처리, `INSERT ... SELECT`, 성능 최적화 SQL
- 예외적인 `JOIN UPDATE` 등 복합 DML
bespoke는 전용 Mapper와 그 Mapper를 호출하는 Service를 함께 둡니다. 단일 테이블 처리와 범용 CRUD는 bespoke마다 중복하지 않고 standard로 올립니다. admin/front의 업무 Service가 정책과 트랜잭션을 소유하며, core bespoke는 복합 데이터 접근 책임에 집중합니다.
## 업무 흐름과 호출 기준
```text
Controller -> admin/front 업무 Service
admin/front 업무 Service -> standard Service
admin/front 업무 Service -> bespoke Service
bespoke Service -> bespoke Mapper
```
- core standard와 core bespoke는 서로의 Service 또는 Mapper를 호출하지 않는다.
- standard와 bespoke를 함께 써야 하는 업무 흐름은 반드시 admin/front 업무 Service가 각각 호출해 조합한다.
- 따라서 core는 독립적인 단일 테이블 접근(standard)과 복합 SQL 접근(bespoke)만 제공하고, 두 계층 사이의 호출 순서나 트랜잭션 경계는 갖지 않는다.
```text
관리자 회원가입
UserController -> admin UserService
-> TestUserService
-> TestUserTokenService
회원과 토큰을 함께 조회하는 관리자 화면
MemberController -> admin MemberService
-> bespoke.testUser.TestUserMemberService
-> TestUserMemberMapper.testUserMemberList
```
- standard는 단일 테이블을 기준으로 실제 `select`, `insert`, `update`, `delete`를 처리한다. 단일 테이블의 인증용 조회처럼 필요한 필드 구성이 다른 경우도 standard의 전용 조회 메서드로 둔다.
- admin/front 업무 Service는 어떤 standard/bespoke를 어떤 순서로 호출할지, 어떤 정책을 적용할지, 어디까지 하나의 트랜잭션으로 묶을지를 결정한다.
- Controller는 core standard/bespoke Service 또는 Mapper를 직접 호출하지 않는다.
- 여러 테이블 조인은 보통 bespoke 조회 Mapper에 둔다.
- 여러 테이블을 하나의 SQL로 갱신해야 하는 `JOIN UPDATE`는 bespoke 수정 Mapper의 예외다.
- standard/bespoke의 첫 판단 기준은 SQL이 접근하는 테이블 수다. 단일 테이블이면 standard, 여러 테이블 JOIN·복합 DML이면 bespoke다. 정책과 트랜잭션 경계는 admin/front 업무 Service가 담당한다.
## 업무 모듈 파일 구조
```text
modules/standard/testUser
├─ mapper
│ └─ TestUserMapper.java
├─ service
│ └─ TestUserService.java
├─ dto
└─ vo
resources/mapper/standard/testUser
└─ TestUserMapper.xml
modules/bespoke/notice
├─ mapper
│ └─ NoticeNoticeMapper.java
├─ service
│ └─ NoticeNoticeService.java
├─ dto
└─ vo
resources/mapper/bespoke/notice
└─ NoticeNoticeMapper.xml
```
- Mapper와 Service는 각각 `mapper`, `service` 패키지로 분리한다.
- DTO와 VO는 `dto`, `vo` 패키지로 분리한다.
- Mapper XML namespace는 `mapper` 패키지를 포함한 Mapper 인터페이스 전체 경로와 일치해야 한다.
## 테이블·업무 모듈 기반 명명
core와 실행 모듈의 Service 이름은 역할을 임의로 줄이지 않고 기준 테이블과 업무 모듈을 조합한다.
```text
standard
{Table}Service
bespoke
{BaseTable}{BusinessModule}Service
```
- `{Table}``{BaseTable}`은 기준 테이블의 Java 이름이다. 예: `test_user` -> `TestUser`, `NOTICE` -> `Notice`.
- standard는 해당 테이블의 기본 CRUD이므로 별도 업무 모듈 접미사를 붙이지 않는다. 예: `TestUserService`, `NoticeService`.
- bespoke는 기준 테이블 뒤에 이를 사용하는 업무 모듈명을 붙인다. 예: `TestUserAuthService`, `TestUserTokenAuthService`, `NoticeSupportService`.
- 업무 모듈명은 `admin`, `front` 같은 실행 서버명이 아니라 `auth`, `support`처럼 실제 기능 목적을 사용한다.
- `Query`, `Custom`, `Bespoke`, `Etc`처럼 처리 방식만 표현하는 접미사는 사용하지 않는다.
- bespoke Service에는 이름에 포함된 업무 모듈 목적에 맞는 메서드만 둔다. `TestUserAuthService`에는 `test_user`를 기준으로 하는 여러 테이블 인증 JOIN 조회만 둔다. 단일 `TEST_USER` 조회는 `TestUserService`에 둔다.
- JOIN이 있더라도 SQL이 시작하고 결과를 소유하는 기준 테이블에 맞춰 bespoke 패키지를 정한다. 예: API Key와 refresh token 기준 인증 JOIN 조회는 `bespoke.testUserToken`에 둔다.
### bespoke 메서드 명명
실행 모듈 Service는 API 업무 흐름을 기준으로, core bespoke는 해당 흐름을 위해 기준 테이블에서 반환하는 데이터 형태를 기준으로 이름을 정한다.
```text
admin/front AuthService.selectLogin
-> core TestUserAuthService.testUserLoginView
-> core TestUserTokenService.updateTestUserTokenModify
-> core TestUserService.updateTestUserModify
```
```text
admin/front Service
{select|insert|update|delete}{기능명}
core bespoke Service / Mapper / XML id
{기준테이블명}{기능명}{List|View|Count|Summary|...}
```
- core bespoke 메서드명에서는 `select`, `insert`, `update`, `delete` 접두어를 사용하지 않는다.
- 기준 테이블명은 반드시 앞에 둔다. 예: `testUserLoginView`, `noticeSupportList`.
- 기능명은 이를 호출하는 admin/front Service의 기능 흐름을 따른다. 한 흐름에서 여러 core 조회가 필요하면 각 기준 테이블명을 붙여 여러 메서드를 호출한다.
- 조회 메서드는 `List`, `View`, `Count`, `Summary`, `Tree` 등 반환 형태를 반드시 마지막에 붙인다.
- 조회 조건은 DTO에 둔다. `ByUserIdx`, `ByApiKey`처럼 조건을 메서드명에 붙이지 않는다.
- standard로 처리 가능한 단일 테이블 CRUD와 조회는 bespoke에 만들지 않는다. 예외적인 bespoke CUD도 같은 명명 방식으로 기준 테이블과 기능명을 사용한다. 예: `noticeSupportAdd`, `noticeSupportModify`.
- bespoke Service 메서드, Mapper 인터페이스 메서드, Mapper XML `id`는 정확히 같은 이름을 사용한다.
실행 모듈에서 하나의 업무 패키지가 여러 테이블 기능을 포함할 때도 테이블별 Service를 분리한다.
```text
admin/modules/support
├─ NoticeController
├─ FaqController
└─ service
├─ NoticeSupportService
└─ FaqSupportService
```
`SupportService` 하나에 Notice와 FAQ 로직을 함께 넣지 않는다. 반면 `admin/modules/auth/AuthService`처럼 하나의 인증 흐름을 조합하는 실행 모듈 업무 Service는 기능명만 사용하며, core standard/bespoke Service를 호출해 처리한다.
## 표준 DTO/VO 규칙
standard의 각 업무 모듈은 API 동작 이름을 기준으로 DTO와 VO를 분리한다. 이 객체들은 core 내부에서 Mapper와 Service 사이, 또는 core Service와 실행 모듈 업무 Service 사이에 전달되는 업무 객체이며 화면 전용 응답 객체가 아니다.
```text
modules/standard/notice
├─ NoticeMapper.java
├─ NoticeService.java
├─ dto
│ ├─ NoticeAddDto.java
│ ├─ NoticeViewDto.java
│ ├─ NoticeListDto.java
│ ├─ NoticeModifyDto.java
│ └─ NoticeDeleteDto.java
└─ vo
├─ NoticeAddVo.java
├─ NoticeViewVo.java
├─ NoticeListVo.java
├─ NoticeListItemVo.java
├─ NoticeModifyVo.java
└─ NoticeDeleteVo.java
```
- 기본 동작명은 `add`, `view`, `list`, `modify`, `delete`로 통일한다.
- 목록은 검색/페이징 조건인 `ListDto`, 목록 행인 `ListItemVo`, 목록과 페이징 결과를 담는 `ListVo`를 분리한다.
- 변경 결과 VO는 동작이 드러나는 boolean과 대상 PK를 함께 둔다. 예: `NoticeModifyVo(updated, noticeIdx)`, `NoticeDeleteVo(deleted, noticeIdx)`. 등록 결과에는 `added`와 생성 PK 등 필요한 결과 값을 함께 둔다.
- admin/front Form은 실행 모듈의 업무 Service까지 전달할 수 있다. 업무 Service는 Form 값을 core DTO로 변환하고, core VO는 필요할 때만 실행 모듈의 화면 전용 VO로 조립한다.
## core VO와 실행 모듈 VO
core VO는 Mapper 조회 결과와 core 내부 업무 처리에 필요한 데이터를 표현한다. admin/front VO는 HTTP 응답으로 실제 클라이언트에 전달할 필드만 표현한다.
```text
core Mapper
-> core VO
-> admin/front 업무 Service
-> admin/front 화면/API VO
-> Controller
```
- core VO는 특정 화면에 묶이지 않고 업무·DB 기준으로 필요한 데이터를 담는다.
- admin/front 업무 Service는 core VO에서 각 API에 필요한 필드만 골라 실행 모듈 VO로 조립한다.
- 같은 core VO라도 admin과 front는 서로 다른 외부 VO를 사용할 수 있다.
- 비밀번호, refresh token, 내부 인증값 같은 민감 정보는 일반 core VO에 기본으로 넣지 않는다. 꼭 필요한 인증 처리에는 전용 core VO를 사용하고, admin/front 외부 VO에는 절대 옮기지 않는다.
- Controller는 core DTO/VO를 import하거나 반환 타입으로 사용하지 않는다. core DTO/VO는 admin/front 업무 Service까지만 전달한다.
- Controller가 필요한 값은 반드시 해당 실행 모듈 Service가 조립한 admin/front VO로 받는다. core 조회 결과를 Controller에서 외부 VO로 변환하지 않는다.
```text
AuthController
-> admin AuthService.selectLoginChecked
-> core TestUserTokenAuthService.testUserTokenLoginCheckedView
-> TestUserTokenLoginCheckedViewVo (core 내부)
-> AuthLoginCheckedVo (admin 외부 응답용)
-> ApiResponse<AuthLoginCheckedVo>
```
## 단일 수정과 상태 변경
단일 필드 변경도 별도 `UseModifyDto`, `StatusModifyDto`를 늘리지 않는다. 기본적으로 하나의 `{Domain}ModifyDto`를 사용한다.
```text
NoticeModifyDto
- noticeIdx // 수정 대상
- noticeType // 선택
- targetScope // 선택
- title // 선택
- content // 선택
- pinYn // 선택
- startDate // 선택
- endDate // 선택
- useYn // 선택
- updateMember // 항상 기록
```
- Service는 DTO에 담긴 값을 그대로 Mapper에 전달한다.
- Mapper XML에서는 `<set>`을 사용하지 않는다. 항상 갱신하는 수정일 컬럼을 `SET`의 첫 항목으로 두고, null이 아닌 선택 필드는 `<if>`와 앞쪽 쉼표로 뒤에 붙인다. 따라서 사용 상태만 바꾸는 경우에도 `NoticeModifyDto``noticeIdx`, `useYn`, `updateMember`만 넣어 호출한다.
- `START_DATE`, `END_DATE`처럼 null로 명시적 초기화가 필요한 컬럼은 단순 null 제외 규칙으로는 처리할 수 없다. 초기화 요구가 생기면 전용 초기화 플래그 또는 해당 업무에 맞는 bespoke SQL을 추가한다.
## 변경 결과 VO
수정과 삭제는 다음 기준으로 결과를 반환한다.
```text
NoticeModifyVo
- updated // update 영향 행 수가 1 이상이면 true
- noticeIdx // 수정 요청 대상 PK
NoticeDeleteVo
- deleted // delete(논리 삭제) 영향 행 수가 1 이상이면 true
- noticeIdx // 삭제 요청 대상 PK
```
- `updated`, `deleted`는 SQL 영향 행 수로 계산한다.
- 변경 결과 VO에는 내부 처리용 `resultCode`를 둔다. 성공은 `2002` 또는 `2005`, 수정값 없음은 `4001`, 대상 없음은 `2003`처럼 Service가 반환한다.
- Controller는 `resultCode`에 맞는 `ApiResponseCode`를 선택한다. `resultCode``@JsonIgnore`로 외부 응답 본문에는 직접 노출하지 않는다.
- PK를 함께 반환하면 호출한 쪽이 실제 처리 대상을 명확히 확인할 수 있다.
## common과 config
- `common`: JWT, 공통 예외, ApiResponse, 페이징, 유틸, 시스템 전반 공통 기능
- `config`: DataSource, MyBatis, 캐시, 필터처럼 공통 인프라를 구성하는 클래스
- 파일 업로드처럼 테이블 CRUD에 속하지 않는 공통 기능은 `common` 또는 `config`의 책임에 맞춰 둡니다.
## admin과 front
```text
api-admin / api-front
├─ Api*Application
├─ config
│ ├─ SecurityConfig
│ └─ OpenApiConfig
└─ modules
└─ Controller, 업무 Service, Form, 화면 VO
```
- Security와 OpenAPI는 서버별 정책과 문서를 가지므로 각 실행 모듈에 둡니다.
- Controller는 Form 검증, 업무 Service 호출, 서버별 응답 VO 조립을 담당합니다.
- 업무 Service는 Form을 받아 필요한 core standard/bespoke DTO로 변환하고, 업무 정책과 트랜잭션을 처리합니다.
+50 -29
View File
@@ -1,38 +1,59 @@
# 프로젝트 개요
## 프로젝트 기본 정보
- 프로젝트 유형: Gradle 기반 Spring Boot 애플리케이션
- 프로젝트 유형: Gradle 기반 Spring Boot 멀티모듈 모놀리식
- Java 버전: 21
- Spring Boot 버전: 3.5.10
- 기본 애플리케이션 이름: `api`
- 기본 포트: `8106`
- 실행 진입점: `src/main/java/com/alist/api/ApiApplication.java`
- 저장소: 단일 Git 저장소
- 실행 서버: admin, front
## 모듈 구성
| 모듈 | 역할 | 실행 여부 |
|---|---|---|
| `01-api-core` | 공통 인프라, 공통 기능, 업무 모듈 | 실행하지 않음 |
| `02-api-admin` | 관리자 API, 관리자 보안과 Swagger 설정 | `api-admin.jar` |
| `03-api-front` | 사용자 API, 사용자 보안과 Swagger 설정 | `api-front.jar` |
admin/front는 `01-api-core`에 의존하지만 서로 의존하지 않습니다. 각 bootJar에는 core 라이브러리가 포함되며, 다른 서버의 Controller는 물리적으로 포함되지 않습니다.
## 패키지 구조
```text
com.alist.api.core
com.alist.api.admin
com.alist.api.front
```
`core`는 다음 책임으로 나눕니다.
- `common`: JWT, 예외, 응답, 페이징, 유틸과 같은 시스템 공통 기능
- `config`: DataSource, MyBatis, 공통 인프라 설정
- `modules.standard`: 재사용 가능한 표준 데이터 처리
- `modules.bespoke`: 기준 테이블의 업무 전용 JOIN·집계·특수 SQL 데이터 접근
`admin``front`에는 Controller, 업무 Service, Form, 외부 응답 VO, 서버별 `SecurityConfig`, `OpenApiConfig`, 환경별 리소스를 둡니다. 업무 정책과 여러 core 호출의 조합, 트랜잭션 경계는 각 실행 모듈 Service가 소유합니다.
admin과 front는 물리적으로 분리된 서버이므로 API path에 서버 구분 접두어를 중복하지 않습니다. 예를 들어 admin 인증 API는 `/admin/auth/login`이 아니라 `/auth/login`으로 둡니다.
## 현재 이관 상태
- 멀티모듈 Gradle 구조와 admin/front 개별 bootJar 구성이 완료되었습니다.
- 공통 응답, 예외, JWT, DataSource, Mapper XML 리소스는 core로 이동했습니다.
- 기존 admin/front 업무 API는 core standard/bespoke와 각 실행 모듈 Service 구조로 이관했습니다.
- 단일 모듈 레거시 소스는 제거했으며, 이전 구현 이력은 Git history로 확인합니다.
## 기술 스택
- **Java**: 21
- **Framework**: Spring Boot 3.5.10
- **빌드 도구**: Gradle
- **DB**: MariaDB
- **ORM**: MyBatis (mapper XML: `classpath:mapper/**/*.xml`)
- **인증**: JWT (jjwt 0.11.5) + Spring Security
- **API 문서**: Swagger (springdoc-openapi 2.8.0)
- **기타**: Lombok, Validation, Actuator, log4jdbc
## 디렉터리 가이드
- `src/main/java/com/alist/api`: 애플리케이션 시작점과 업무 코드를 둔다.
- `src/main/java/com/alist/api/modules`: 기능별 모듈 패키지를 둔다.
- `src/main/resources`: 설정 파일과 로깅 설정을 관리한다.
- `deploy`: 배포 관련 리소스가 있으면 이 경로를 우선 확인한다.
- Java 21, Spring Boot 3.5.10, Gradle
- MyBatis, MariaDB, SQL Server 마이그레이션 데이터소스
- Spring Security, JWT, Redis
- Swagger / OpenAPI, Actuator, Lombok, Validation, log4jdbc
## 현재 확인된 구조
- 현재 기준 메인 흐름은 `Controller -> Form -> Dto -> Service -> Mapper(XML) -> Vo -> Service -> Controller` 순서로 연결된다.
- API 에서 request 받을 때 POST 는 주로 JSON을 사용한다. Controller 는 `form` 객체로 요청을 받은 뒤 DTO 로 변환해서 Service 에 전달한다.
- MyBatis는 인터페이스와 XML을 함께 사용한다.
- Mapper 인터페이스는 `src/main/java/.../mapper`, SQL XML은 `src/main/resources/mapper/...` 경로를 짝으로 맞춘다.
- 공통 응답은 `common/response`, 보안은 `config/security, jwt`, 전역 예외 처리는 `config/exception` 아래에 둔다.
- 모듈 패키지는 현재 `admin`, `cors`, `file`, `front`, `main`, `migration`, `tusFile` 형태로 구성되어 있고, 필요한 모듈만 `dto`, `form`, `mapper`, `service`, `vo`를 둔다.
- `front` 하위에는 사용자 인증/SSO(`auth`)와 사용자 관리(`user`) 흐름을 둔다.
- `admin` 하위에는 관리자 인증, 회원, 사용자, 스케줄, SSO 클라이언트 관리 흐름을 둔다.
- `cors` 모듈은 DB 기반 허용 Origin 관리와 캐시 갱신 API를 담당한다.
- `file` 모듈은 DB 기록 없는 단순 업로드, SunEditor 이미지 업로드, uploadPath 기반 view/download를 담당한다.
- `tusFile` 모듈은 DB 기록이 필요한 TUS 기반 대용량 업로드 초기화, 업로드 토큰 검증, tusd hook, 상태 조회, 파일 목록/view/download/delete 흐름을 담당한다.
## 빌드 결과
```text
02-api-admin/build/libs/api-admin.jar
03-api-front/build/libs/api-front.jar
```
+72 -35
View File
@@ -1,43 +1,80 @@
# 설정 및 실행 가이드
## 설정 규칙
- 공통 설정은 `application.yaml`, 환경별 차이는 `application-local.yaml`, `application-pjt.yaml` 에 둔다.
- `pjt` 프로파일은 DB/Redis/JWT/Swagger 계정을 환경변수 치환으로 받으므로 새 민감정보는 하드코딩하지 않는다.
- 로깅 설정은 프로파일별 `logback-local.xml`, `logback-pjt.xml` 을 사용하므로 로그 정책 변경 시 함께 본다.
- MyBatis 설정은 `application.yaml` 기준으로 관리하므로 mapper location, alias package, camel case 옵션을 중복 정의하지 않는다.
- Redis 는 세션 저장소와 업로드 상태 캐시 용도를 함께 가지므로 키 prefix 충돌 여부를 확인한다.
## 설정 위치
- 환경값은 각 실행 모듈의 `src/main/resources`에 둡니다.
- 공통 설정은 `application.yaml`, 환경별 차이는 `application-local.yaml`, `application-pjt.yaml`에 둡니다.
- admin과 front는 서로 다른 포트, 도메인, JWT 쿠키 정책, Swagger 정보, 로그 설정을 가질 수 있습니다.
- core에는 실행 환경별 YAML을 두지 않고, 환경값을 사용하는 공통 구현과 인프라 설정만 둡니다.
- 단순 설정값은 사용하는 클래스에서 `@Value` 필드 주입으로 읽습니다. 목록·Map·중첩 구조처럼 구조화된 설정만 `@ConfigurationProperties` 클래스로 관리합니다.
## 프로파일
| 프로파일 | 설명 |
|---------|------|
|---|---|
| `local` | 로컬 개발 환경 |
| `pjt` | 프로젝트(개발) 환경 |
## Swagger 접속
- URL: `http://localhost:8106/swagger-ui.html`
- 인증: `swagger.login.id` / `swagger.login.password` (환경별 yaml에 설정)
## 파일 업로드 설정
- `tus-file.upload.final-root` 는 TUS 완료 파일과 단순 업로드 파일이 공유하는 최종 저장 루트다.
- `tus-file.upload.tmp-root` 는 tusd 임시 업로드 루트다. file-domain nginx 에서는 `/uploads/tmp/` 접근을 404로 막는다.
- `file.upload.root-path` 는 보통 `${tus-file.upload.final-root}` 를 사용해 단순 업로드와 TUS 완료 파일의 마운트 루트를 맞춘다.
- `file.upload.view.file-domain` 은 SunEditor 이미지 응답 URL 생성에 사용한다. 예: `https://file-alist.pjt.kr`.
- `file.upload.max-size` 는 단순 업로드 전역 최대 용량이다. `file.upload.types.{folder}.max-size` 가 있으면 폴더별 설정이 우선한다.
- `file.upload.allowed-extensions` 는 단순 업로드 전역 확장자 허용 목록이다.
- `file.upload.types.{key}.folder` 는 실제 저장 폴더명이다. 설정되지 않은 folder 값도 전역 정책을 통과하면 동적 폴더로 저장할 수 있다.
- `file.upload.types.{key}.image-only``true` 이면 이미지 확장자만 허용한다.
- `file.upload.types.{key}.resize.enabled``true` 이고 업로드 파일이 이미지이면 resize 함수를 거친다. `width``height` 가 모두 있으면 중앙 crop 후 고정 크기로 저장하고, `max-width` 만 있으면 비율을 유지해 축소한다.
- `spring.servlet.multipart.max-file-size``max-request-size``-1` 로 두고, 실제 제한은 업로드 서비스 정책에서 처리한다.
## file-domain nginx 기준
- `/tus/file/` 는 tusd 로 프록시하며 `auth_request /_upload_auth` 로 API의 `/tus/file/upload/auth` 를 호출한다.
- `/_upload_auth` 는 내부 location 으로만 열고 `Authorization`, `X-File-Uuid`, 필요 시 `Upload-Metadata`, `Upload-Length` 헤더를 API 로 전달한다.
- `/uploads/``/srv/project/alist/uploads/` 를 정적 파일로 제공한다.
- `/uploads/tmp/` 는 tusd 임시 파일 노출을 막기 위해 404 처리한다.
- 그 외 경로는 `location /` fallback 에서 차단한다.
| `pjt` | 프로젝트 개발 환경 |
## 실행 명령
- 로컬 실행: `./gradlew bootRun`
- 테스트 실행: `./gradlew test`
- jar 생성: `./gradlew bootJar`
- Windows 명령: `.\gradlew.bat bootRun`, `.\gradlew.bat test`, `.\gradlew.bat bootJar`
```bash
./gradlew :02-api-admin:bootRun --args='--spring.profiles.active=local'
./gradlew :03-api-front:bootRun --args='--spring.profiles.active=local'
```
```bash
./gradlew :02-api-admin:bootJar
./gradlew :03-api-front:bootJar
```
## Docker 배포
- admin과 front는 각각 독립 JAR와 Docker 이미지를 사용합니다.
- admin Dockerfile은 `02-api-admin/Dockerfile`, front Dockerfile은 `03-api-front/Dockerfile`에 둡니다.
- Compose는 `deploy/pjt/compose/` 아래에서 서버별로 분리합니다.
```bash
./gradlew :02-api-admin:bootJar
docker build -t registry.pjt.kr/alist/api-admin:${IMAGE_TAG} 02-api-admin
./gradlew :03-api-front:bootJar
docker build -t registry.pjt.kr/alist/api-front:${IMAGE_TAG} 03-api-front
```
| 구분 | Admin | Front |
|---|---|---|
| Compose | `api-admin-compose.pjt.yml` | `api-front-compose.pjt.yml` |
| 컨테이너 포트 | `8111` | `8112` |
| 환경파일 | `/srv/project/alist/env/api-admin/.env` | `/srv/project/alist/env/api-front/.env` |
| 로그 파일 | `/srv/project/alist/logs/api-admin/app/api-admin.log` | `/srv/project/alist/logs/api-front/app/api-front.log` |
두 서버 모두 파일 저장 경로를 공유해야 하므로 `/srv/project/alist/uploads`를 같은 컨테이너 경로로 마운트합니다.
## Jenkins 배포
- 루트 `Jenkinsfile.pjt`는 프로젝트 환경 배포용이며, `TARGET` 파라미터로 `admin`, `front`, `all`을 선택합니다.
- 선택한 대상만 Gradle `bootJar`, Docker image build/push, 원격 Compose 배포를 수행합니다.
- admin과 front는 서로 다른 Compose project(`alist-api-admin`, `alist-api-front`)로 실행하므로 한쪽 배포가 다른 서버 컨테이너를 내리지 않습니다.
- Jenkins 서버에는 `registry-pjt`, `ssh-pjt` credential이 필요합니다.
## Swagger
- admin: `http://localhost:8111/swagger-ui.html`
- front: `http://localhost:8112/swagger-ui.html`
- 인증 정보는 각 모듈 환경 설정의 `swagger.login.id`, `swagger.login.password`를 사용합니다.
## MyBatis와 리소스
- main Mapper XML은 `01-api-core/src/main/resources/mapper/standard/**/*.xml` 또는 `mapper/bespoke/**/*.xml`에 둡니다.
- `MainDataSourceConfig`가 위 두 경로만 명시적으로 스캔합니다. admin/front YAML의 MyBatis 공통 속성보다 core의 `MainDataSourceConfig` 설정을 기준으로 확인합니다.
- migration Mapper XML은 `01-api-core/src/main/resources/mapper/migration/**`에 두며, migration 전용 SqlSessionFactory가 `classpath*:` 기준으로 로드합니다. main DataSource 스캔 대상은 아닙니다.
- migration DataSource는 front의 `migration.datasource.alist.enabled`, `migration.datasource.eltown.enabled``true`인 경우에만 활성화합니다. admin YAML에는 migration 설정을 두지 않습니다.
- Mapper Java 인터페이스, 내부 DTO/VO, XML namespace와 XML id는 core 업무 모듈 이관 시 함께 이동합니다.
## 파일 업로드와 TUS
- 공통 저장 루트와 파일 조회 도메인은 `file.storage.root-path`, `file.storage.view-domain`으로 관리하며 admin/front 양쪽에 둡니다.
- 단순 파일 업로드의 확장자, 크기, 타입별 정책은 `file.upload.*`으로 관리합니다.
- TUS 설정(`tus-file.upload.*`)은 TUS를 실행하는 admin 서버에만 둡니다. Front YAML에는 두지 않습니다.
- TUS 완료 파일은 `file.storage.root-path` 아래로 이동하고, tusd 임시 파일은 `tus-file.upload.tmp-root` 아래에 둡니다.
- file-domain nginx와 tusd 설정은 `/tus/file/**` 경로, 최종 저장 루트, 임시 저장 루트가 일치하는지 함께 확인합니다.
+39 -32
View File
@@ -1,39 +1,46 @@
# 보안 및 응답 규칙
## 예외/응답 규칙
- 공통 예외 응답은 `GlobalExceptionHandler` 에서 처리하므로 Controller 별 개별 예외 처리를 중복해서 늘리지 않는다.
- `@Valid`, 바인딩 실패, JSON 파싱 실패, 타입 오류는 `CODE_4001` 로 통일하고 필드 오류가 있으면 `Map<String, String>` 형태로 반환한다.
- 단순 성공/실패 문자열을 직접 내려주기보다 `ApiResponse.entity(...)` `ApiResponseCode` 조합을 우선 사용다.
- 404/405/500 같은 공통 HTTP 오류도 가능하면 `ApiResponseCode` enum 으로 맞춘다.
## 예외와 응답
- 공통 예외 응답은 `GlobalExceptionHandler`에서 처리합니다.
- Controller는 `ApiResponse.entity(...)``ApiResponseCode` 조합을 우선 사용합니다.
- 바이너리 파일 view/download처럼 필요한 경우만 `ResponseEntity<Resource>`를 직접 반환합니다.
- `@Valid`, 바인딩 실패, JSON 파싱 실패, 타입 오류는 `CODE_4001`로 통일합니다.
## 보안 구조
- Swagger: `/v3/api-docs/**`, `/swagger-ui/**` → HTTP Basic 인증 (InMemory)
- API: JWT Bearer 토큰 인증 (Stateless)
- 세션/쿠키: Redis Session 저장소 사용, 쿠키 속성은 프로파일별 `cookie.*` 설정으로 제어
- Admin API: `/admin/**` 는 별도 `SecurityFilterChain` 으로 분리하며 `/admin/auth/**`, `/admin/user/add` 만 공개하고 나머지는 `ADMIN` 권한을 요구한다.
- 공개 경로: `/`, `/actuator/health`, `/sso/**`, `/auth/**`, `/user/add`, `/user/migration/list`, `/tus/file/hook`, `/tus/file/upload/auth`, `/file/**`
- TUS 업로드 토큰은 일반 access token 이 아니므로 `/tus/file/upload/auth`, `/tus/file/hook` 은 공개 경로와 file-domain nginx `auth_request` 설정을 함께 맞춘다.
- `JwtAuthenticationFilter` 는 shared API(`/tus/file/**`, `/file/**`, `/cors/**`)에서 user access token 또는 admin access token 모두 인증 주체로 받을 수 있다.
- `/admin/auth/loginChecked` 는 admin access token 쿠키의 현재 로그인 상태 확인용이다. 토큰을 재발급하지 않으며 `isAdminAccessToken`, `isAdminRefreshToken`, `loggedIn`, `userId`, `userIdx`, `userTokenIdx`, `userRole` 형태의 값을 반환한다.
- Swagger 인증과 API 인증은 `SecurityFilterChain` 을 분리해서 관리한다.
## 응답 코드 규칙
- 응답 코드는 `ApiResponseCode` enum 으로 관리한다.
- admin과 front는 각각 독립된 `SecurityConfig``SecurityFilterChain`을 가집니다.
- Swagger 경로는 HTTP Basic으로 별도 보호합니다.
- API는 JWT Bearer 토큰 또는 서버별 HttpOnly 쿠키를 사용합니다.
- admin JWT 필터는 `ADMIN` scope와 `adminAccessToken`만 인증합니다.
- front JWT 필터는 `USER` scope와 `accessToken`만 인증합니다.
- JWT 필터는 URL(`/admin/**` 등)로 서버 종류를 판단하지 않습니다.
- 각 서버의 공개 경로와 역할 정책은 해당 서버 `SecurityConfig`에서 관리합니다.
- admin/front는 물리적으로 분리되어 있으므로 API path에 `/admin` 또는 `/front` 접두어를 두지 않습니다. 서버 역할은 배포 대상과 `SecurityConfig`가 구분합니다.
| 코드 | 메시지 | HTTP Status | 용도 |
|------|--------|-------------|------|
| `CODE_200` | 성공 | 200 OK | 일반 성공 |
| `CODE_400` | 잘못된 요청 | 400 Bad Request | 일반 클라이언트 오류 |
| `CODE_401` | 인증 필요 합니다. | 401 Unauthorized | 인증 없음 |
| `CODE_403` | 접근 권한 필요 합니다. | 403 Forbidden | 권한 없음 |
| `CODE_404` | 페이지를 찾을 수 없습니다. | 404 Not Found | 리소스 없음 |
| `CODE_405` | 잘못된 요청입니다. 요청 방식을 확인해 주세요. | 405 Method Not Allowed | 메서드 불일치 |
| `CODE_500` | 요청을 처리하는 중 오류가 발생했습니다. | 500 Internal Server Error | 서버 오류 |
| `CODE_2001` | {0} 정보 조회에 성공하였습니다. | 200 OK | 단건 조회 성공 |
| `CODE_2002` | {0} 등록 되었습니다. | 201 Created | 등록 성공 |
| `CODE_2003` | 조회된 정보가 없습니다. | 200 OK | 조회 결과 없음 |
| `CODE_2004` | 중복된 {0} 정보 입니다. | 409 Conflict | 중복 데이터 |
| `CODE_4001` | 입력값을 확인해주세요. | 400 Bad Request | `@Valid` / 바인딩 / 타입오류 / JSON 파싱 실패 |
| `CODE_4003` | 필수 요청 파라미터가 누락되었습니다. | 400 Bad Request | 필수 파라미터 누락 |
## 서버별 정책
- `{0}` 자리에 대상명 삽입 (예: `CODE_2001` → "회원 정보 조회에 성공하였습니다.")
- admin API는 기본적으로 `ADMIN` 역할을 요구합니다.
- front API는 사용자 토큰을 인증 주체로 사용합니다.
- `/actuator/health`는 각 서버의 헬스체크 경로입니다.
- TUS는 admin 서버 이관 대상이며, front에는 TUS 공개 경로를 두지 않습니다.
## 응답 코드
응답 코드는 `ApiResponseCode` enum으로 관리합니다.
| 코드 | HTTP Status | 용도 |
|---|---|---|
| `CODE_200` | 200 OK | 일반 성공 |
| `CODE_400` | 400 Bad Request | 일반 클라이언트 오류 |
| `CODE_401` | 401 Unauthorized | 인증 없음 |
| `CODE_403` | 403 Forbidden | 권한 없음 |
| `CODE_404` | 404 Not Found | 리소스 없음 |
| `CODE_405` | 405 Method Not Allowed | 메서드 불일치 |
| `CODE_500` | 500 Internal Server Error | 서버 오류 |
| `CODE_2001` | 200 OK | 단건 조회 성공 |
| `CODE_2002` | 201 Created | 등록 성공 |
| `CODE_2003` | 200 OK | 조회 결과 없음 |
| `CODE_2004` | 409 Conflict | 중복 데이터 |
| `CODE_4001` | 400 Bad Request | 입력값, 바인딩, 타입 오류 |
| `CODE_4003` | 400 Bad Request | 필수 요청 파라미터 누락 |
+89
View File
@@ -0,0 +1,89 @@
# 테스트 전략
## 목적
- 기능 이관과 신규 개발에서 테스트 범위를 과도하게 넓히지 않고 위험도에 맞춰 검증한다.
- 단위 테스트, 통합 테스트, 실제 연동 검증의 책임을 분리한다.
- 기능 또는 모듈을 완료할 때 실제 동작과 데이터 반영까지 확인한다.
## 테스트 구분
| 구분 | 목적 | 외부 의존성 | 실행 시점 |
|---|---|---|---|
| 단위 테스트 | 분기, 결과 코드, DTO/VO 변환, 권한 판단 검증 | 사용하지 않음 | 구현 중 수시 실행 |
| 통합 테스트 | Spring, MyBatis, Security, DB와 모듈 간 연결 검증 | 테스트 DB 등 필요한 의존성만 사용 | 모듈 기능 완료 시 |
| 실제 연동 검증 | 실행 API와 실제 데이터 저장·갱신 결과 확인 | 기능이 사용하는 실제 의존성만 사용 | 모듈 이관 완료 후 |
## Red-Green 흐름
1. 기능의 핵심 성공·실패 조건을 테스트로 먼저 작성한다.
2. 테스트가 실패하는 Red 상태를 확인한다.
3. 최소 구현으로 테스트를 통과하는 Green 상태를 만든다.
4. 필요할 때만 리팩터링하고 같은 테스트를 다시 실행한다.
이미 이관이 끝난 기능에는 기존 동작을 고정하는 회귀 테스트를 추가한다. 구현된 기능을 의도적으로 깨뜨려 Red 상태를 재현하지 않는다.
## 단위 테스트 기준
- 단순 CRUD 보일러플레이트마다 테스트를 강제하지 않는다.
- 다음 중 하나가 있으면 단위 테스트를 우선 추가한다.
- 권한 또는 역할 분기
- 중복·상태·입력값 검증
- 여러 DTO/VO 간 변환
- 날짜, 금액, 카운트 등 계산
- 실패 결과 코드와 예외 처리
- 외부 DB, Redis, 파일 시스템, HTTP 호출은 Mock 또는 Fake로 대체한다.
## 통합 테스트 기준
- 모듈 단위 작업이 완료되면 해당 실행 모듈과 Core를 함께 검증한다.
- MyBatis Mapper XML, namespace, SQL 결과 매핑, Spring Bean 구성, Security 경로를 확인한다.
- 테스트 환경은 개발 공용 데이터가 아닌 전용 테스트 DB와 테스트 Redis를 우선 사용한다.
- Redis Pub/Sub처럼 커밋 후 동작이 필요한 기능은 실제 트랜잭션 커밋과 수신 결과를 검증한다.
## 실제 연동 검증 기준
실제 연동 검증은 모든 기능에 같은 항목을 강제하지 않는다.
```text
기본: API 호출 -> DB 저장 또는 변경 확인 -> 조회 API 확인
Redis 사용 기능: Redis Key, 캐시, Pub/Sub 수신 결과 추가 확인
파일 사용 기능: 파일 저장, 조회, 삭제, 경로 처리 추가 확인
외부 API 기능: 요청, 응답, 실패 처리 추가 확인
메시지 기능: 발행, 수신, 재처리 결과 추가 확인
```
- 실제 개발 데이터를 변경해야 하면 테스트용 식별값을 사용한다.
- 등록·수정·삭제 테스트 후에는 테스트 데이터를 삭제하거나 원복한다.
- 검증하지 못한 외부 의존성은 이유와 미검증 범위를 남긴다.
## 실행 순서
```text
기능 구현 중
-> 관련 모듈 compileJava
-> 필요한 단위 테스트
모듈 작업 완료
-> Core 및 실행 모듈 build
-> 통합 테스트
-> 실제 API / DB / 기능별 의존성 검증
배포 전
-> Admin / Front 전체 빌드
-> 핵심 시나리오 회귀 테스트
```
## Gradle 기준
- 현재 최소 컴파일 검증 명령
```bash
./gradlew :01-api-core:compileJava
./gradlew :02-api-admin:compileJava
./gradlew :03-api-front:compileJava
```
- 단위 테스트와 통합 테스트가 추가되면 `unitTest`, `integrationTest` 태스크로 분리한다.
- 일상 개발에서는 빠른 단위 테스트를 우선 실행하고, 모듈 완료·배포 전에는 통합 테스트까지 실행한다.
+44 -22
View File
@@ -1,26 +1,48 @@
# 검증 및 체크리스트
## 기본 검증 원칙
- 현재 테스트 코드는 최소 수준이므로 기능 수정 시 단위 테스트 또는 최소 통합 검증 범위를 직접 보강하는 쪽을 우선한다.
- 기동 실패 가능성이 있는 설정 변경은 실행 또는 테스트로 검증한다.
- 검증하지 못한 내용은 추정으로 말하지 않고 미실행 사유를 적는다.
## 기본 검증
## 변경 시 체크리스트
- 변경한 코드와 직접 관련된 파일만 수정했는지 확인한다.
- 새 API, 스케줄러, 설정 추가 시 관련 설정 파일과 테스트를 함께 검토한다.
- 로그 레벨과 로그량이 운영 환경에서 감당 가능한지 확인한다.
- Mapper 인터페이스 추가/변경 시 XML namespace, id, parameter/result 매핑이 같이 맞는지 확인한다.
- 공개 경로나 권한 정책을 바꿨다면 SecurityConfig 와 Swagger 노출 범위를 같이 확인한다.
- 파일 업로드/다운로드 기능 수정 시 DB 상태, Redis 상태, 실제 파일 시스템 경로가 같이 맞는지 확인한다.
- TUS 경로를 바꾸면 nginx `/_upload_auth`, tusd hook URL, `SecurityConfig` 공개 경로, `tus-file.upload.tus-endpoint` 를 함께 확인한다.
- `/file/**`, `/tus/file/**`, `/cors/**` 같은 shared API 권한을 바꾸면 user/admin 토큰 쿠키와 Bearer 인증이 모두 의도대로 동작하는지 확인한다.
- SunEditor 또는 단순 업로드 설정을 바꾸면 `file.upload.root-path`, `file.upload.view.file-domain`, nginx `/uploads/` alias 경로가 같은 저장 루트를 가리키는지 확인한다.
- 변경 범위와 직접 관련된 모듈만 수정했는지 확인합니다.
- core 변경은 `:01-api-core:compileJava`를, 실행 모듈 변경은 `:02-api-admin:compileJava`, `:03-api-front:compileJava`를 최소 검증으로 사용합니다.
- 실행 설정이나 빈 구성이 바뀌면 admin/front를 각각 기동하거나 관련 테스트를 실행합니다.
- 검증하지 못한 항목은 사유를 명확히 남깁니다.
## 기능 특성별 점검 포인트
- 스케줄러 코드는 실행 주기, 중복 실행 가능성, 로그량을 반드시 점검한다.
- 인증 방식이 섞여 있으므로 세션 기반 처리와 JWT `SecurityContext` 사용 위치를 먼저 구분하고 수정한다.
- 파일 경로를 다루는 기능은 상대경로 탈출, 루트 이탈 방지 같은 검증을 같이 본다.
- 설정 파일 수정 시 `local`, `pjt`, 공통 설정 간 차이를 함께 확인한다.
- file-domain 정적 파일은 `/uploads/editor/...` 같은 최종 파일 URL이 브라우저에서 직접 열리는지 확인한다.
- `/uploads/tmp/...` 는 404로 막히는지 확인한다.
- SunEditor 업로드는 응답 JSON의 `result[].url` 이 file-domain 절대 URL인지 확인하고, 에디터 본문에 이미지가 실제 삽입되는지 확인한다.
## 모듈 완료 검증
- 구현 중에는 관련 모듈의 `compileJava`와 필요한 단위 테스트를 수시 실행합니다.
- 모듈 작업을 완료하면 Core와 해당 실행 모듈의 `build`를 실행합니다.
- 통합 테스트는 Spring, MyBatis, Security, DB 등 해당 기능의 연결 범위를 검증합니다.
- 실제 연동 검증은 `API -> DB -> 조회 API`를 기본으로 합니다.
- Redis, 파일, 외부 API, 메시지 등은 해당 기능이 실제로 사용할 때만 추가 검증합니다.
- 실제 개발 데이터를 변경한 테스트는 테스트 데이터를 삭제하거나 원복합니다.
## 업무 이관
- Java Mapper, Mapper XML, namespace, id, DTO 패키지 참조를 함께 변경합니다.
- 범용 처리는 `standard`, 업무 전용 처리는 `bespoke` 기준으로 배치했는지 확인합니다.
- core에 Form, 화면 전용 VO, Controller가 들어가지 않았는지 확인합니다.
- admin/front에 다른 서버 전용 Controller가 포함되지 않았는지 확인합니다.
- Controller가 core DTO/VO, core Service 또는 Mapper를 직접 참조하지 않는지 확인합니다.
- admin/front Service가 core standard/bespoke 호출 조합과 트랜잭션 경계를 소유하는지 확인합니다.
- bespoke Service/Mapper/XML id가 기준 테이블과 기능명, 결과 형태 규칙을 같은 이름으로 사용하는지 확인합니다.
## 보안
- admin은 `ADMIN` scope와 `adminAccessToken`으로 인증되는지 확인합니다.
- front는 `USER` scope와 `accessToken`으로 인증되는지 확인합니다.
- 공개 경로와 역할 정책은 각 서버 `SecurityConfig`에서 확인합니다.
- Swagger Basic 인증과 API JWT 인증이 각각 의도대로 동작하는지 확인합니다.
## 설정과 리소스
- admin/front의 `application*.yaml`, 로그 설정, banner, robots 리소스가 각 모듈에 있는지 확인합니다.
- core Mapper XML이 두 bootJar에서 모두 classpath로 읽히는지 확인합니다.
- `MainDataSourceConfig``mapper/standard`, `mapper/bespoke`의 이관된 XML만 읽는지 확인합니다.
- migration DataSource를 사용하지 않는 환경에서는 `migration.datasource.*.enabled`가 활성화되지 않았는지 확인합니다.
- 파일 또는 TUS를 변경하면 저장 경로, nginx, tusd, SecurityConfig를 함께 확인합니다.
## 빌드 산출물
- `:02-api-admin:bootJar` 결과가 `api-admin.jar`인지 확인합니다.
- `:03-api-front:bootJar` 결과가 `api-front.jar`인지 확인합니다.
- admin JAR에 front Controller가, front JAR에 admin Controller가 포함되지 않는지 확인합니다.