Merge pull request 'rere' (#6) from feat/dean/test7 into develop
Reviewed-on: https://10.100.0.10:3210/dabeeo/kamco-dabeeo-backoffice/pulls/6
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = tab
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
@@ -9,4 +9,4 @@ trim_trailing_whitespace = true
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.java]
|
[*.java]
|
||||||
max_line_length = 100
|
max_line_length = 180
|
||||||
|
|||||||
212
CLAUDE.md
Normal file
212
CLAUDE.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**kamco-change-detection-api** is a Spring Boot 3.5.7 application using Java 21 and PostgreSQL for KAMCO's change detection system. The project handles geospatial data with JTS/GeoJSON integration and uses QueryDSL for type-safe database queries.
|
||||||
|
|
||||||
|
## Build & Run Commands
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
# Full build with tests
|
||||||
|
./gradlew build
|
||||||
|
|
||||||
|
# Build without tests (Jenkins CI uses this)
|
||||||
|
./gradlew clean build -x test
|
||||||
|
|
||||||
|
# Build JAR (creates ROOT.jar)
|
||||||
|
./gradlew bootJar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Application
|
||||||
|
```bash
|
||||||
|
# Run with local profile (default)
|
||||||
|
./gradlew bootRun
|
||||||
|
|
||||||
|
# Run with specific profile
|
||||||
|
./gradlew bootRun --args='--spring.profiles.active=dev'
|
||||||
|
# or
|
||||||
|
java -jar build/libs/ROOT.jar --spring.profiles.active=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Run specific test class
|
||||||
|
./gradlew test --tests com.kamco.cd.kamcoback.KamcoBackApplicationTests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Framework**: Spring Boot 3.5.7
|
||||||
|
- **Language**: Java 21
|
||||||
|
- **Database**: PostgreSQL
|
||||||
|
- **Connection Pool**: HikariCP
|
||||||
|
- **ORM**: Spring Data JPA + Hibernate
|
||||||
|
- **Query DSL**: QueryDSL 5.0.0 (Jakarta)
|
||||||
|
- **Geospatial**: JTS (Java Topology Suite) with GeoJSON serialization
|
||||||
|
- **Build Tool**: Gradle
|
||||||
|
- **Monitoring**: Spring Boot Actuator
|
||||||
|
|
||||||
|
### Configuration Profiles
|
||||||
|
|
||||||
|
The application uses **profile-based configuration** with three environments:
|
||||||
|
|
||||||
|
| Profile | File | Purpose | Active Profiles |
|
||||||
|
|---------|------|---------|----------------|
|
||||||
|
| `local` | application.yml | Local development | default |
|
||||||
|
| `dev` | application-dev.yml | Development server | `--spring.profiles.active=dev` |
|
||||||
|
| `prod` | application.yml | Production server | `--spring.profiles.active=prod` |
|
||||||
|
|
||||||
|
**CRITICAL**: Profile names must match configuration exactly:
|
||||||
|
- ✅ Use `dev` (not `develop`)
|
||||||
|
- ✅ Use `prod` (not `production`)
|
||||||
|
- ✅ Use `local` (default, not `localhost`)
|
||||||
|
|
||||||
|
### Geospatial Data Handling
|
||||||
|
|
||||||
|
The application has **custom Jackson serializers/deserializers** for JTS Geometry types:
|
||||||
|
|
||||||
|
**Location**: `com.kamco.cd.kamcoback.common.utils.geometry`
|
||||||
|
|
||||||
|
- `GeometrySerializer`: Converts JTS Geometry → GeoJSON with **16-digit precision** (increased from default 8 digits)
|
||||||
|
- `GeometryDeserializer`: Converts GeoJSON → JTS Geometry types (Point, Polygon, Geometry)
|
||||||
|
|
||||||
|
**Registered for types**: `Geometry`, `Point`, `Polygon`
|
||||||
|
|
||||||
|
**Configuration**: `WebConfig.java` configures ObjectMapper bean with custom module
|
||||||
|
|
||||||
|
**Usage in entities**: Use JTS geometry types directly in JPA entities:
|
||||||
|
```java
|
||||||
|
@Column(columnDefinition = "geometry(Point,4326)")
|
||||||
|
private Point location;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "geometry(Polygon,4326)")
|
||||||
|
private Polygon boundary;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
**HikariCP Connection Pool** is configured per profile:
|
||||||
|
|
||||||
|
| Setting | Local | Dev | Prod |
|
||||||
|
|---------|-------|-----|------|
|
||||||
|
| Min Idle | 10 | 5 | N/A |
|
||||||
|
| Max Pool Size | 50 | 20 | N/A |
|
||||||
|
| Connection Timeout | 20s | Default | Default |
|
||||||
|
| Idle Timeout | 5min | Default | Default |
|
||||||
|
| Max Lifetime | 30min | Default | Default |
|
||||||
|
|
||||||
|
**JPA/Hibernate Settings**:
|
||||||
|
- DDL Auto: `validate` (schema changes require manual migration)
|
||||||
|
- JDBC Batch Size: 50
|
||||||
|
- Default Batch Fetch Size: 100 (N+1 query prevention)
|
||||||
|
- Show SQL: `true` in dev, configurable per profile
|
||||||
|
|
||||||
|
**Startup Logging**: `StartupLogger.java` displays full configuration on startup including active profiles, database connection, pool settings, and JPA configuration.
|
||||||
|
|
||||||
|
### QueryDSL Setup
|
||||||
|
|
||||||
|
**Dependencies** (build.gradle):
|
||||||
|
- Implementation: `querydsl-jpa:5.0.0:jakarta`
|
||||||
|
- Annotation Processor: `querydsl-apt:5.0.0:jakarta`
|
||||||
|
|
||||||
|
**Q-Class Generation**: Q-classes are generated via annotation processors for Jakarta Persistence entities.
|
||||||
|
|
||||||
|
**Pattern** (from TODO): Future repositories should follow `CustomRepository` + `CustomRepositoryImpl` pattern for complex queries.
|
||||||
|
|
||||||
|
### Actuator Monitoring
|
||||||
|
|
||||||
|
**Endpoints** configured at `/monitor` base path:
|
||||||
|
- `/monitor/health` - Health checks with readiness/liveness probes
|
||||||
|
- JMX endpoints excluded for security
|
||||||
|
|
||||||
|
**Health Probes**: Enabled for Kubernetes-style readiness and liveness checks.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
**Standard**: Google Java Style Guide with project-specific modifications
|
||||||
|
|
||||||
|
**Key Rules**:
|
||||||
|
- **Indentation**: 2 spaces (not tabs)
|
||||||
|
- **Line Length**: 100 characters max
|
||||||
|
- **Line Endings**: LF (Unix-style)
|
||||||
|
- **Charset**: UTF-8
|
||||||
|
- **Trailing Whitespace**: Removed
|
||||||
|
- **Final Newline**: Required
|
||||||
|
|
||||||
|
**Style XML**: `intellij-java-google-style.xml` (IntelliJ IDEA code style configuration)
|
||||||
|
|
||||||
|
### Automated Code Formatting
|
||||||
|
|
||||||
|
**Spotless Gradle Plugin**: Automatically enforces Google Java Format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check formatting
|
||||||
|
./gradlew spotlessCheck
|
||||||
|
|
||||||
|
# Apply formatting
|
||||||
|
./gradlew spotlessApply
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pre-commit Hook**: Automatically checks formatting before each commit
|
||||||
|
- Location: `.git/hooks/pre-commit`
|
||||||
|
- Runs `spotlessCheck` before allowing commits
|
||||||
|
- If formatting fails, run `./gradlew spotlessApply` to fix
|
||||||
|
|
||||||
|
**IntelliJ Setup**: See `CODE_STYLE_SETUP.md` for detailed instructions on:
|
||||||
|
- Importing code style XML
|
||||||
|
- Enabling format-on-save
|
||||||
|
- Configuring Actions on Save
|
||||||
|
|
||||||
|
**Important**: The project uses **spaces** (not tabs) for indentation, with 2-space indent size.
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
com.kamco.cd.kamcoback
|
||||||
|
├── KamcoBackApplication.java # Main application entry point
|
||||||
|
├── config/
|
||||||
|
│ ├── WebConfig.java # Jackson ObjectMapper + Geometry serializers
|
||||||
|
│ └── StartupLogger.java # Application startup diagnostics
|
||||||
|
└── common/
|
||||||
|
├── api/ # API DTOs
|
||||||
|
└── utils/
|
||||||
|
└── geometry/ # GeoJSON serialization utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
### Current State (Early Stage)
|
||||||
|
The project is in initial setup phase. From `common/README.md`, planned but not yet implemented:
|
||||||
|
- QueryDSL custom/impl repository pattern
|
||||||
|
- Common code management system
|
||||||
|
- Redis caching layer
|
||||||
|
|
||||||
|
### When Adding New Features
|
||||||
|
1. **Entities with Geometry**: Use JTS types directly; serialization is handled automatically
|
||||||
|
2. **Repository Queries**: Use QueryDSL for type-safe queries (Q-classes auto-generated)
|
||||||
|
3. **Configuration**: Add profile-specific settings in appropriate application-{profile}.yml
|
||||||
|
4. **Database Changes**: DDL is set to `validate`; schema changes need manual migration
|
||||||
|
|
||||||
|
### Profile Activation Troubleshooting
|
||||||
|
If you see "Failed to configure a DataSource", check:
|
||||||
|
1. Profile name matches configuration file exactly (`dev`, `prod`, or `local`)
|
||||||
|
2. DataSource URL, username, password are set in profile configuration
|
||||||
|
3. PostgreSQL JDBC driver is on classpath (runtimeOnly dependency)
|
||||||
|
|
||||||
|
### Testing Geometry Serialization
|
||||||
|
Use the configured ObjectMapper bean for JSON operations. Manual ObjectMapper creation will miss custom geometry serializers.
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
**Jenkins Pipeline**: `Jenkinsfile-dev`
|
||||||
|
- Branch: `develop`
|
||||||
|
- Build: `./gradlew clean build -x test`
|
||||||
|
- JDK: Java 21
|
||||||
|
- Artifact: `ROOT.jar`
|
||||||
112
CODE_STYLE_SETUP.md
Normal file
112
CODE_STYLE_SETUP.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Code Style 설정 가이드
|
||||||
|
|
||||||
|
이 문서는 프로젝트에서 Google Java Style을 자동으로 적용하기 위한 설정 가이드입니다.
|
||||||
|
|
||||||
|
## 자동 포맷팅 구성
|
||||||
|
|
||||||
|
### 1. 커밋 시점 자동 포맷팅 (Git Pre-commit Hook)
|
||||||
|
|
||||||
|
커밋 전에 자동으로 코드를 포맷팅하고 스테이징합니다.
|
||||||
|
|
||||||
|
**설정 완료:** `.git/hooks/pre-commit` 파일이 자동으로 실행됩니다.
|
||||||
|
|
||||||
|
**동작 방식:**
|
||||||
|
- 커밋 시도 시 `./gradlew spotlessApply` 자동 실행
|
||||||
|
- 스테이징된 Java 파일을 자동으로 포맷팅
|
||||||
|
- 포맷팅된 파일을 자동으로 다시 스테이징
|
||||||
|
- 포맷팅이 완료되면 커밋 진행
|
||||||
|
|
||||||
|
**장점:**
|
||||||
|
- 수동으로 `spotlessApply`를 실행할 필요 없음
|
||||||
|
- 항상 일관된 코드 스타일 유지
|
||||||
|
- 포맷팅 누락 방지
|
||||||
|
|
||||||
|
### 2. IntelliJ IDEA 저장 시점 자동 포맷팅
|
||||||
|
|
||||||
|
#### 방법 1: Code Style 설정 임포트 (권장)
|
||||||
|
|
||||||
|
1. **IntelliJ IDEA 열기**
|
||||||
|
2. **Settings/Preferences** (Mac: `⌘,` / Windows: `Ctrl+Alt+S`)
|
||||||
|
3. **Editor > Code Style > Java**
|
||||||
|
4. **⚙️ (톱니바퀴)** 클릭 > **Import Scheme > IntelliJ IDEA code style XML**
|
||||||
|
5. 프로젝트 루트의 `intellij-java-google-style.xml` 파일 선택
|
||||||
|
6. **OK** 클릭
|
||||||
|
|
||||||
|
#### 방법 2: 저장 시 자동 포맷팅 활성화
|
||||||
|
|
||||||
|
|
||||||
|
**Option A: Actions on Save 설정**
|
||||||
|
|
||||||
|
1. **Settings/Preferences** > **Tools > Actions on Save**
|
||||||
|
2. 다음 옵션들을 활성화:
|
||||||
|
- ✅ **Reformat code**
|
||||||
|
- ✅ **Optimize imports**
|
||||||
|
- ✅ **Rearrange code** (선택사항)
|
||||||
|
3. **Changed lines** 또는 **Whole file** 선택
|
||||||
|
4. **OK** 클릭
|
||||||
|
|
||||||
|
**Option B: Save Actions Plugin 사용 (더 많은 옵션)**
|
||||||
|
|
||||||
|
1. **Settings/Preferences** > **Plugins**
|
||||||
|
2. **Marketplace**에서 "Save Actions" 검색 및 설치
|
||||||
|
3. **Settings/Preferences** > **Other Settings > Save Actions**
|
||||||
|
4. 다음 옵션 활성화:
|
||||||
|
- ✅ **Activate save actions on save**
|
||||||
|
- ✅ **Reformat file**
|
||||||
|
- ✅ **Optimize imports**
|
||||||
|
- ✅ **Rearrange fields and methods** (선택사항)
|
||||||
|
|
||||||
|
### 3. Gradle Spotless Plugin 수동 실행
|
||||||
|
|
||||||
|
#### 코드 포맷팅 체크
|
||||||
|
```bash
|
||||||
|
# 포맷팅 문제 확인만 (수정하지 않음)
|
||||||
|
./gradlew spotlessCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 코드 자동 포맷팅
|
||||||
|
```bash
|
||||||
|
# 모든 Java 파일 자동 포맷팅 적용
|
||||||
|
./gradlew spotlessApply
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 빌드 시 자동 체크
|
||||||
|
```bash
|
||||||
|
# 빌드 전에 자동으로 spotlessCheck 실행됨
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 코드 스타일 규칙
|
||||||
|
|
||||||
|
프로젝트는 **Google Java Style Guide** 기반으로 다음 규칙을 따릅니다:
|
||||||
|
|
||||||
|
- **Indentation**: 2 spaces (탭 아님)
|
||||||
|
- **Line Length**: 180 characters
|
||||||
|
- **Line Endings**: LF (Unix-style)
|
||||||
|
- **Charset**: UTF-8
|
||||||
|
- **Import Order**: Static imports → 빈 줄 → Regular imports
|
||||||
|
- **Braces**: 모든 if, for, while, do 문에 중괄호 필수
|
||||||
|
|
||||||
|
## 문제 해결
|
||||||
|
|
||||||
|
### Pre-commit hook이 실행되지 않는 경우
|
||||||
|
```bash
|
||||||
|
# 실행 권한 확인 및 부여
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spotless 플러그인이 동작하지 않는 경우
|
||||||
|
```bash
|
||||||
|
# Gradle 의존성 다시 다운로드
|
||||||
|
./gradlew clean build --refresh-dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### IntelliJ 포맷팅이 다르게 적용되는 경우
|
||||||
|
1. `intellij-java-google-style.xml` 다시 임포트
|
||||||
|
2. **File > Invalidate Caches** > **Invalidate and Restart**
|
||||||
|
|
||||||
|
## 추가 정보
|
||||||
|
|
||||||
|
- **Google Java Style Guide**: https://google.github.io/styleguide/javaguide.html
|
||||||
|
- **Spotless Plugin**: https://github.com/diffplug/spotless
|
||||||
|
- **IntelliJ Code Style**: https://www.jetbrains.com/help/idea/code-style.html
|
||||||
@@ -193,10 +193,6 @@ kamco-back/
|
|||||||
│ │ │ │ └── StartupLogger.java # 시작 로그
|
│ │ │ │ └── StartupLogger.java # 시작 로그
|
||||||
│ │ │ ├── common/ # 공통 유틸리티
|
│ │ │ ├── common/ # 공통 유틸리티
|
||||||
│ │ │ │ └── utils/geometry/ # GeoJSON 직렬화
|
│ │ │ │ └── utils/geometry/ # GeoJSON 직렬화
|
||||||
│ │ │ ├── domain/ # 도메인 엔티티 (예정)
|
|
||||||
│ │ │ ├── repository/ # 리포지토리 (예정)
|
|
||||||
│ │ │ ├── service/ # 비즈니스 로직 (예정)
|
|
||||||
│ │ │ └── controller/ # REST 컨트롤러 (예정)
|
|
||||||
│ │ └── resources/
|
│ │ └── resources/
|
||||||
│ │ ├── application.yml # 기본 설정 + local/prod
|
│ │ ├── application.yml # 기본 설정 + local/prod
|
||||||
│ │ └── application-dev.yml # 개발 환경 설정
|
│ │ └── application-dev.yml # 개발 환경 설정
|
||||||
|
|||||||
19
build.gradle
19
build.gradle
@@ -2,6 +2,7 @@ plugins {
|
|||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.5.7'
|
id 'org.springframework.boot' version '3.5.7'
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
|
id 'com.diffplug.spotless' version '6.25.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'com.kamco.cd'
|
group = 'com.kamco.cd'
|
||||||
@@ -48,6 +49,9 @@ dependencies {
|
|||||||
|
|
||||||
// actuator
|
// actuator
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
|
|
||||||
|
// SpringDoc OpenAPI (Swagger)
|
||||||
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
@@ -58,3 +62,18 @@ tasks.named('test') {
|
|||||||
bootJar {
|
bootJar {
|
||||||
archiveFileName = 'ROOT.jar'
|
archiveFileName = 'ROOT.jar'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spotless configuration for code formatting (2-space indent)
|
||||||
|
spotless {
|
||||||
|
java {
|
||||||
|
target 'src/**/*.java'
|
||||||
|
googleJavaFormat('1.19.2') // Default Google Style = 2 spaces (NO .aosp()!)
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run spotlessCheck before build
|
||||||
|
tasks.named('build') {
|
||||||
|
dependsOn 'spotlessCheck'
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<package name="" withSubpackages="true" static="false" />
|
<package name="" withSubpackages="true" static="false" />
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="RIGHT_MARGIN" value="100" />
|
<option name="RIGHT_MARGIN" value="180" />
|
||||||
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
|
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
|
||||||
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
|
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
|
||||||
<option name="JD_P_AT_EMPTY_LINES" value="false" />
|
<option name="JD_P_AT_EMPTY_LINES" value="false" />
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
|
pluginManagement {
|
||||||
|
plugins {
|
||||||
|
id 'org.jetbrains.kotlin.jvm' version '2.2.20'
|
||||||
|
}
|
||||||
|
}
|
||||||
rootProject.name = 'kamco-back'
|
rootProject.name = 'kamco-back'
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ public class KamcoBackApplication {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(KamcoBackApplication.class, args);
|
SpringApplication.run(KamcoBackApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kamco.cd.kamcoback.common.api;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.api.HelloDto.Res;
|
||||||
|
import com.kamco.cd.kamcoback.common.service.HelloService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/hello")
|
||||||
|
public class HelloApiController {
|
||||||
|
|
||||||
|
private final HelloService helloService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public HelloDto.Res hello(HelloDto.Req req) {
|
||||||
|
req.valid();
|
||||||
|
|
||||||
|
Res res = helloService.sayHello(req);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,36 @@
|
|||||||
package com.kamco.cd.kamcoback.common.api;
|
package com.kamco.cd.kamcoback.common.api;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
public class HelloDto {
|
public class HelloDto {
|
||||||
|
|
||||||
public static class Req{
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Req {
|
||||||
private String id;
|
private String id;
|
||||||
|
|
||||||
|
public void valid() {
|
||||||
|
if (id == null) {
|
||||||
|
throw new IllegalArgumentException(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Res {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public Res(String id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.kamco.cd.kamcoback.common.service;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Core Service Interface
|
||||||
|
*
|
||||||
|
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
|
||||||
|
*
|
||||||
|
* @param <T> Entity 타입
|
||||||
|
* @param <ID> Entity의 ID 타입
|
||||||
|
* @param <S> Search Request 타입
|
||||||
|
*/
|
||||||
|
public interface BaseCoreService<T, ID, S> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID로 엔티티를 삭제합니다.
|
||||||
|
*
|
||||||
|
* @param id 삭제할 엔티티의 ID
|
||||||
|
*/
|
||||||
|
void remove(ID id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID로 단건 조회합니다.
|
||||||
|
*
|
||||||
|
* @param id 조회할 엔티티의 ID
|
||||||
|
* @return 조회된 엔티티
|
||||||
|
*/
|
||||||
|
T getOneById(ID id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 조건과 페이징으로 조회합니다.
|
||||||
|
*
|
||||||
|
* @param searchReq 검색 조건
|
||||||
|
* @return 페이징 처리된 검색 결과
|
||||||
|
*/
|
||||||
|
Page<T> search(S searchReq);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.kamco.cd.kamcoback.common.service;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.api.HelloDto;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class HelloService {
|
||||||
|
|
||||||
|
public HelloDto.Res sayHello(HelloDto.Req req) {
|
||||||
|
log.info("hello");
|
||||||
|
String name = UUID.randomUUID().toString();
|
||||||
|
return HelloDto.Res.builder().id(req.getId()).name(name).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,34 +4,33 @@ import com.fasterxml.jackson.core.JacksonException;
|
|||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
import java.io.IOException;
|
||||||
import org.locationtech.jts.geom.Geometry;
|
import org.locationtech.jts.geom.Geometry;
|
||||||
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
import org.locationtech.jts.io.geojson.GeoJsonReader;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
|
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
|
||||||
|
|
||||||
public GeometryDeserializer(Class<T> targetType) {
|
public GeometryDeserializer(Class<T> targetType) {
|
||||||
super(targetType);
|
super(targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test code
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||||
|
throws IOException, JacksonException {
|
||||||
|
String json = jsonParser.readValueAsTree().toString();
|
||||||
|
|
||||||
|
if (!StringUtils.hasText(json)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test code
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
GeoJsonReader reader = new GeoJsonReader();
|
||||||
@Override
|
return (T) reader.read(json);
|
||||||
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
} catch (Exception e) {
|
||||||
throws IOException, JacksonException {
|
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
|
||||||
String json = jsonParser.readValueAsTree().toString();
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(json)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
GeoJsonReader reader = new GeoJsonReader();
|
|
||||||
return (T) reader.read(json);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,29 @@ package com.kamco.cd.kamcoback.common.utils.geometry;
|
|||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
import org.locationtech.jts.geom.Geometry;
|
import org.locationtech.jts.geom.Geometry;
|
||||||
import org.locationtech.jts.io.geojson.GeoJsonWriter;
|
import org.locationtech.jts.io.geojson.GeoJsonWriter;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
|
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
|
||||||
|
|
||||||
// TODO: test code
|
// TODO: test code
|
||||||
public GeometrySerializer(Class<T> targetType) {
|
public GeometrySerializer(Class<T> targetType) {
|
||||||
super(targetType);
|
super(targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serialize(T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
public void serialize(
|
||||||
throws IOException {
|
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
|
||||||
if (Objects.nonNull(geometry)) {
|
throws IOException {
|
||||||
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
|
if (Objects.nonNull(geometry)) {
|
||||||
GeoJsonWriter writer = new GeoJsonWriter(16);
|
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
|
||||||
String json = writer.write(geometry);
|
GeoJsonWriter writer = new GeoJsonWriter(16);
|
||||||
jsonGenerator.writeRawValue(json);
|
String json = writer.write(geometry);
|
||||||
} else {
|
jsonGenerator.writeRawValue(json);
|
||||||
jsonGenerator.writeNull();
|
} else {
|
||||||
}
|
jsonGenerator.writeNull();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.kamco.cd.kamcoback.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.Components;
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI kamcoOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(
|
||||||
|
new Info()
|
||||||
|
.title("KAMCO Change Detection API")
|
||||||
|
.description(
|
||||||
|
"KAMCO 변화 탐지 시스템 API 문서\n\n"
|
||||||
|
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
|
||||||
|
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
|
||||||
|
.version("v1.0.0")
|
||||||
|
// .contact(new Contact().name("KAMCO Development
|
||||||
|
// Team").email("dev@kamco.com").url("https://kamco.com"))
|
||||||
|
// .license(new License().name("Proprietary").url("https://kamco.com/license"))
|
||||||
|
)
|
||||||
|
.servers(
|
||||||
|
List.of(
|
||||||
|
new Server().url("http://localhost:8080").description("로컬 개발 서버")
|
||||||
|
|
||||||
|
// , new Server().url("https://dev-api.kamco.com").description("개발 서버")
|
||||||
|
// , new Server().url("https://api.kamco.com").description("운영 서버")
|
||||||
|
))
|
||||||
|
.components(new Components());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.kamco.cd.kamcoback.config;
|
package com.kamco.cd.kamcoback.config;
|
||||||
|
|
||||||
|
|
||||||
import com.zaxxer.hikari.HikariDataSource;
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -15,85 +14,83 @@ import org.springframework.stereotype.Component;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class StartupLogger {
|
public class StartupLogger {
|
||||||
|
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
public void logStartupInfo() {
|
public void logStartupInfo() {
|
||||||
String[] activeProfiles = environment.getActiveProfiles();
|
String[] activeProfiles = environment.getActiveProfiles();
|
||||||
String profileInfo =
|
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
|
||||||
activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
|
|
||||||
|
|
||||||
// Database connection information
|
// Database connection information
|
||||||
String dbUrl = environment.getProperty("spring.datasource.url");
|
String dbUrl = environment.getProperty("spring.datasource.url");
|
||||||
String dbUsername = environment.getProperty("spring.datasource.username");
|
String dbUsername = environment.getProperty("spring.datasource.username");
|
||||||
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
|
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
|
||||||
|
|
||||||
// HikariCP pool settings
|
// HikariCP pool settings
|
||||||
String poolInfo = "";
|
String poolInfo = "";
|
||||||
if (dataSource instanceof HikariDataSource hikariDs) {
|
if (dataSource instanceof HikariDataSource hikariDs) {
|
||||||
poolInfo =
|
poolInfo =
|
||||||
String.format(
|
String.format(
|
||||||
"""
|
"""
|
||||||
│ Pool Size : min=%d, max=%d
|
│ Pool Size : min=%d, max=%d
|
||||||
│ Connection Timeout: %dms
|
│ Connection Timeout: %dms
|
||||||
│ Idle Timeout : %dms
|
│ Idle Timeout : %dms
|
||||||
│ Max Lifetime : %dms""",
|
│ Max Lifetime : %dms""",
|
||||||
hikariDs.getMinimumIdle(),
|
hikariDs.getMinimumIdle(),
|
||||||
hikariDs.getMaximumPoolSize(),
|
hikariDs.getMaximumPoolSize(),
|
||||||
hikariDs.getConnectionTimeout(),
|
hikariDs.getConnectionTimeout(),
|
||||||
hikariDs.getIdleTimeout(),
|
hikariDs.getIdleTimeout(),
|
||||||
hikariDs.getMaxLifetime());
|
hikariDs.getMaxLifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
// JPA/Hibernate settings
|
// JPA/Hibernate settings
|
||||||
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
|
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
|
||||||
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
|
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
|
||||||
String batchSize =
|
String batchSize =
|
||||||
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
|
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
|
||||||
String batchFetchSize =
|
String batchFetchSize =
|
||||||
environment.getProperty(
|
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
|
||||||
"spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
|
|
||||||
|
|
||||||
String startupMessage =
|
String startupMessage =
|
||||||
String.format(
|
String.format(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
╔════════════════════════════════════════════════════════════════════════════════╗
|
╔════════════════════════════════════════════════════════════════════════════════╗
|
||||||
║ 🚀 APPLICATION STARTUP INFORMATION ║
|
║ 🚀 APPLICATION STARTUP INFORMATION ║
|
||||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||||
║ PROFILE CONFIGURATION ║
|
║ PROFILE CONFIGURATION ║
|
||||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||||
│ Active Profile(s): %s
|
│ Active Profile(s): %s
|
||||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||||
║ DATABASE CONFIGURATION ║
|
║ DATABASE CONFIGURATION ║
|
||||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||||
│ Database URL : %s
|
│ Database URL : %s
|
||||||
│ Username : %s
|
│ Username : %s
|
||||||
│ Driver : %s
|
│ Driver : %s
|
||||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||||
║ HIKARICP CONNECTION POOL ║
|
║ HIKARICP CONNECTION POOL ║
|
||||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||||
%s
|
%s
|
||||||
╠════════════════════════════════════════════════════════════════════════════════╣
|
╠════════════════════════════════════════════════════════════════════════════════╣
|
||||||
║ JPA/HIBERNATE CONFIGURATION ║
|
║ JPA/HIBERNATE CONFIGURATION ║
|
||||||
╠────────────────────────────────────────────────────────────────────────────────╣
|
╠────────────────────────────────────────────────────────────────────────────────╣
|
||||||
│ Show SQL : %s
|
│ Show SQL : %s
|
||||||
│ DDL Auto : %s
|
│ DDL Auto : %s
|
||||||
│ JDBC Batch Size : %s
|
│ JDBC Batch Size : %s
|
||||||
│ Fetch Batch Size : %s
|
│ Fetch Batch Size : %s
|
||||||
╚════════════════════════════════════════════════════════════════════════════════╝
|
╚════════════════════════════════════════════════════════════════════════════════╝
|
||||||
""",
|
""",
|
||||||
profileInfo,
|
profileInfo,
|
||||||
dbUrl != null ? dbUrl : "N/A",
|
dbUrl != null ? dbUrl : "N/A",
|
||||||
dbUsername != null ? dbUsername : "N/A",
|
dbUsername != null ? dbUsername : "N/A",
|
||||||
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
|
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
|
||||||
poolInfo,
|
poolInfo,
|
||||||
showSql,
|
showSql,
|
||||||
ddlAuto,
|
ddlAuto,
|
||||||
batchSize,
|
batchSize,
|
||||||
batchFetchSize);
|
batchFetchSize);
|
||||||
|
|
||||||
log.info(startupMessage);
|
log.info(startupMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfig {
|
public class WebConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ObjectMapper objectMapper() {
|
public ObjectMapper objectMapper() {
|
||||||
SimpleModule module = new SimpleModule();
|
SimpleModule module = new SimpleModule();
|
||||||
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
|
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
|
||||||
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
|
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
|
||||||
|
|
||||||
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
|
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
|
||||||
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
|
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
|
||||||
|
|
||||||
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
|
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
|
||||||
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
|
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
|
||||||
|
|
||||||
return Jackson2ObjectMapperBuilder.json()
|
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
|
||||||
.modulesToInstall(module)
|
}
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.kamco.cd.kamcoback.config.api;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
|
||||||
|
*
|
||||||
|
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(
|
||||||
|
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
// ApiResponseDto를 반환하는 경우에만 적용
|
||||||
|
return returnType.getParameterType().equals(ApiResponseDto.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object beforeBodyWrite(
|
||||||
|
Object body,
|
||||||
|
MethodParameter returnType,
|
||||||
|
MediaType selectedContentType,
|
||||||
|
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||||
|
ServerHttpRequest request,
|
||||||
|
ServerHttpResponse response) {
|
||||||
|
|
||||||
|
if (body instanceof ApiResponseDto<?> apiResponse) {
|
||||||
|
// ApiResponseDto에 설정된 httpStatus를 실제 HTTP 응답에 적용
|
||||||
|
response.setStatusCode(apiResponse.getHttpStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.kamco.cd.kamcoback.config.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.kamco.cd.kamcoback.config.enums.EnumType;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class ApiResponseDto<T> {
|
||||||
|
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
private Error error;
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
private T errorData;
|
||||||
|
|
||||||
|
@JsonIgnore private HttpStatus httpStatus = HttpStatus.OK;
|
||||||
|
|
||||||
|
public ApiResponseDto(T data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiResponseDto(T data, HttpStatus httpStatus) {
|
||||||
|
this.data = data;
|
||||||
|
this.httpStatus = httpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponseDto(ApiResponseCode code) {
|
||||||
|
this.error = new Error(code.getId(), code.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponseDto(ApiResponseCode code, String message) {
|
||||||
|
this.error = new Error(code.getId(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
|
||||||
|
this.error = new Error(code.getId(), message);
|
||||||
|
this.errorData = errorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
|
||||||
|
public static <T> ApiResponseDto<T> createOK(T data) {
|
||||||
|
return new ApiResponseDto<>(data, HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponseDto<T> ok(T data) {
|
||||||
|
return new ApiResponseDto<>(data, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponseDto<T> deleteOk(T data) {
|
||||||
|
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApiResponseDto<String> createException(ApiResponseCode code) {
|
||||||
|
return new ApiResponseDto<>(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
|
||||||
|
return new ApiResponseDto<>(code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponseDto<T> createException(
|
||||||
|
ApiResponseCode code, String message, T data) {
|
||||||
|
return new ApiResponseDto<>(code, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Error {
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
public Error(String code, String message) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum ApiResponseCode implements EnumType {
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
OK("요청이 성공하였습니다."),
|
||||||
|
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
|
||||||
|
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
|
||||||
|
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
|
||||||
|
UNAUTHORIZED("권한이 없습니다."),
|
||||||
|
CONFLICT("이미 등록된 컨텐츠입니다."),
|
||||||
|
NOT_FOUND("Resource를 찾을 수 없습니다."),
|
||||||
|
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
|
||||||
|
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
|
||||||
|
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
|
||||||
|
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
|
||||||
|
UNAUTHENTICATED("인증에 실패하였습니다."),
|
||||||
|
INVALID_TOKEN("잘못된 토큰입니다."),
|
||||||
|
EXPIRED_TOKEN("만료된 토큰입니다."),
|
||||||
|
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
|
||||||
|
FORBIDDEN("권한을 확인해주세요."),
|
||||||
|
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
|
||||||
|
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
|
||||||
|
WRONG_STATUS("잘못된 상태입니다."),
|
||||||
|
FAIL_VERIFICATION("인증에 실패하였습니다."),
|
||||||
|
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
|
||||||
|
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
|
||||||
|
WRONG_PASSWORD("잘못된 패스워드입니다.."),
|
||||||
|
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
|
||||||
|
DUPLICATE_DATA("이미 등록되여 있습니다."),
|
||||||
|
DATA_INTEGRITY_ERROR("요청을 처리할수 없습니다."),
|
||||||
|
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
|
||||||
|
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
|
||||||
|
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
|
||||||
|
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
|
||||||
|
INVALID_EMAIL_TOKEN(
|
||||||
|
"You can only reset your password within 24 hours from when the email was sent.\n"
|
||||||
|
+ "To reset your password again, please submit a new request through \"Forgot"
|
||||||
|
+ " Password.\""),
|
||||||
|
;
|
||||||
|
// @formatter:on
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.kamco.cd.kamcoback.config.enums;
|
||||||
|
|
||||||
|
public interface EnumType {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
String getText();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@MappedSuperclass
|
||||||
|
public class CommonDateEntity {
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_date", updatable = false, nullable = false)
|
||||||
|
private ZonedDateTime createdDate;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "modified_date", nullable = false)
|
||||||
|
private ZonedDateTime modifiedDate;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onPersist() {
|
||||||
|
this.createdDate = ZonedDateTime.now();
|
||||||
|
this.modifiedDate = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.modifiedDate = ZonedDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres;
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Configuration
|
||||||
|
public class QueryDslConfig {
|
||||||
|
|
||||||
|
private final EntityManager entityManager;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JPAQueryFactory jpaQueryFactory() {
|
||||||
|
return new JPAQueryFactory(entityManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.core;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.service.BaseCoreService;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.repository.AnimalRepository;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.repository.ZooRepository;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AnimalCoreService
|
||||||
|
implements BaseCoreService<AnimalDto.Basic, Long, AnimalDto.SearchReq> {
|
||||||
|
|
||||||
|
private final AnimalRepository animalRepository;
|
||||||
|
private final ZooRepository zooRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public AnimalDto.Basic getDataByUuid(String uuid) {
|
||||||
|
AnimalEntity getZoo =
|
||||||
|
animalRepository
|
||||||
|
.getAnimalByUuid(uuid)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Zoo not found with uuid: " + uuid));
|
||||||
|
return getZoo.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddReq를 받는 추가 메서드
|
||||||
|
@Transactional
|
||||||
|
public AnimalDto.Basic create(AnimalDto.AddReq req) {
|
||||||
|
ZooEntity zoo = null;
|
||||||
|
if (req.getZooId() != null) {
|
||||||
|
zoo =
|
||||||
|
zooRepository
|
||||||
|
.getZooByUid(req.getZooId())
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new EntityNotFoundException(" not found with id: " + req.getZooId()));
|
||||||
|
}
|
||||||
|
AnimalEntity entity = new AnimalEntity(req.getCategory(), req.getSpecies(), req.getName(), zoo);
|
||||||
|
AnimalEntity saved = animalRepository.save(entity);
|
||||||
|
return saved.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void remove(Long id) {
|
||||||
|
AnimalEntity getAnimal =
|
||||||
|
animalRepository
|
||||||
|
.getAnimalByUid(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("getAnimal not found with id: " + id));
|
||||||
|
getAnimal.deleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimalDto.Basic getOneById(Long id) {
|
||||||
|
AnimalEntity getAnimal =
|
||||||
|
animalRepository
|
||||||
|
.getAnimalByUid(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Zoo not found with id: " + id));
|
||||||
|
return getAnimal.toDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<AnimalDto.Basic> search(AnimalDto.SearchReq searchReq) {
|
||||||
|
|
||||||
|
Page<AnimalEntity> animalEntities = animalRepository.listAnimal(searchReq);
|
||||||
|
return animalEntities.map(AnimalEntity::toDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.core;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.common.service.BaseCoreService;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.repository.ZooRepository;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ZooCoreService implements BaseCoreService<ZooDto.Detail, Long, ZooDto.SearchReq> {
|
||||||
|
|
||||||
|
private final ZooRepository zooRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ZooDto.Detail getDataByUuid(String uuid) {
|
||||||
|
ZooEntity zoo =
|
||||||
|
zooRepository
|
||||||
|
.getZooByUuid(uuid)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Zoo not found with uuid: " + uuid));
|
||||||
|
return toDetailDto(zoo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddReq를 받는 추가 메서드
|
||||||
|
@Transactional
|
||||||
|
public ZooDto.Detail create(ZooDto.AddReq req) {
|
||||||
|
ZooEntity entity = new ZooEntity(req.getName(), req.getLocation(), req.getDescription());
|
||||||
|
ZooEntity saved = zooRepository.save(entity);
|
||||||
|
return toDetailDto(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void remove(Long id) {
|
||||||
|
ZooEntity zoo =
|
||||||
|
zooRepository
|
||||||
|
.getZooByUid(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Zoo not found with id: " + id));
|
||||||
|
zoo.deleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZooDto.Detail getOneById(Long id) {
|
||||||
|
ZooEntity zoo =
|
||||||
|
zooRepository
|
||||||
|
.getZooByUid(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Zoo not found with id: " + id));
|
||||||
|
return toDetailDto(zoo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<ZooDto.Detail> search(ZooDto.SearchReq searchReq) {
|
||||||
|
Page<ZooEntity> zooEntities = zooRepository.listZoo(searchReq);
|
||||||
|
|
||||||
|
// N+1 문제 해결: 한 번의 쿼리로 모든 Zoo의 animal count 조회
|
||||||
|
List<Long> zooIds =
|
||||||
|
zooEntities.getContent().stream().map(ZooEntity::getUid).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<Long, Long> animalCountMap = zooRepository.countActiveAnimalsByZooIds(zooIds);
|
||||||
|
|
||||||
|
// DTO 변환
|
||||||
|
List<ZooDto.Detail> details =
|
||||||
|
zooEntities.getContent().stream()
|
||||||
|
.map(zoo -> toDetailDtoWithCountMap(zoo, animalCountMap))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new PageImpl<>(details, zooEntities.getPageable(), zooEntities.getTotalElements());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity -> Detail DTO 변환 (동물 개수 포함) - 단건 조회용
|
||||||
|
private ZooDto.Detail toDetailDto(ZooEntity zoo) {
|
||||||
|
Long activeAnimalCount = zooRepository.countActiveAnimals(zoo.getUid());
|
||||||
|
return new ZooDto.Detail(
|
||||||
|
zoo.getUid(),
|
||||||
|
zoo.getUuid().toString(),
|
||||||
|
zoo.getName(),
|
||||||
|
zoo.getLocation(),
|
||||||
|
zoo.getDescription(),
|
||||||
|
zoo.getCreatedDate(),
|
||||||
|
zoo.getModifiedDate(),
|
||||||
|
activeAnimalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity -> Detail DTO 변환 (동물 개수 포함) - 배치 조회용 (N+1 해결)
|
||||||
|
private ZooDto.Detail toDetailDtoWithCountMap(ZooEntity zoo, Map<Long, Long> countMap) {
|
||||||
|
Long activeAnimalCount = countMap.getOrDefault(zoo.getUid(), 0L);
|
||||||
|
return new ZooDto.Detail(
|
||||||
|
zoo.getUid(),
|
||||||
|
zoo.getUuid().toString(),
|
||||||
|
zoo.getName(),
|
||||||
|
zoo.getLocation(),
|
||||||
|
zoo.getDescription(),
|
||||||
|
zoo.getCreatedDate(),
|
||||||
|
zoo.getModifiedDate(),
|
||||||
|
activeAnimalCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.entity;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Category;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Species;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
// 기본구조 관련
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Table(name = "tb_animal")
|
||||||
|
public class AnimalEntity extends CommonDateEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long uid;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Category category;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Species species;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private Boolean isDeleted;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "zoo_id")
|
||||||
|
private ZooEntity zoo;
|
||||||
|
|
||||||
|
// Construct
|
||||||
|
public AnimalEntity(Category category, Species species, String name, ZooEntity zoo) {
|
||||||
|
this.uuid = UUID.randomUUID();
|
||||||
|
this.category = category;
|
||||||
|
this.species = species;
|
||||||
|
this.name = name;
|
||||||
|
this.isDeleted = false;
|
||||||
|
this.zoo = zoo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimalDto.Basic toDto() {
|
||||||
|
return new AnimalDto.Basic(
|
||||||
|
this.uid,
|
||||||
|
this.uuid.toString(),
|
||||||
|
this.name,
|
||||||
|
this.category,
|
||||||
|
this.species,
|
||||||
|
super.getCreatedDate(),
|
||||||
|
super.getModifiedDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleted() {
|
||||||
|
this.isDeleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.entity;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
|
||||||
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Table(name = "tb_zoo")
|
||||||
|
public class ZooEntity extends CommonDateEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long uid;
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 200)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(length = 300)
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean isDeleted;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "zoo", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
|
||||||
|
private List<AnimalEntity> animals = new ArrayList<>();
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
public ZooEntity(String name, String location, String description) {
|
||||||
|
this.uuid = UUID.randomUUID();
|
||||||
|
this.name = name;
|
||||||
|
this.location = location;
|
||||||
|
this.description = description;
|
||||||
|
this.isDeleted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 논리 삭제
|
||||||
|
public void deleted() {
|
||||||
|
this.isDeleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 활성 동물 개수 조회 (삭제되지 않은 동물만)
|
||||||
|
public long getActiveAnimalCount() {
|
||||||
|
return animals.stream().filter(animal -> !animal.getIsDeleted()).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface AnimalRepository
|
||||||
|
extends JpaRepository<AnimalEntity, Long>, AnimalRepositoryCustom {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
public interface AnimalRepositoryCustom {
|
||||||
|
|
||||||
|
Optional<AnimalEntity> getAnimalByUid(Long uid);
|
||||||
|
|
||||||
|
Optional<AnimalEntity> getAnimalByUuid(String uuid);
|
||||||
|
|
||||||
|
Page<AnimalEntity> listAnimal(AnimalDto.SearchReq req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.QAnimalEntity;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Category;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Species;
|
||||||
|
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
|
||||||
|
|
||||||
|
public class AnimalRepositoryImpl extends QuerydslRepositorySupport
|
||||||
|
implements AnimalRepositoryCustom {
|
||||||
|
|
||||||
|
private final JPAQueryFactory queryFactory;
|
||||||
|
|
||||||
|
public AnimalRepositoryImpl(JPAQueryFactory queryFactory) {
|
||||||
|
super(AnimalEntity.class);
|
||||||
|
this.queryFactory = queryFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AnimalEntity> getAnimalByUid(Long uid) {
|
||||||
|
QAnimalEntity animal = QAnimalEntity.animalEntity;
|
||||||
|
|
||||||
|
return Optional.ofNullable(
|
||||||
|
queryFactory.selectFrom(animal).where(animal.uid.eq(uid)).fetchFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AnimalEntity> getAnimalByUuid(String uuid) {
|
||||||
|
QAnimalEntity animal = QAnimalEntity.animalEntity;
|
||||||
|
|
||||||
|
return Optional.ofNullable(
|
||||||
|
queryFactory.selectFrom(animal).where(animal.uuid.eq(UUID.fromString(uuid))).fetchFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<AnimalEntity> listAnimal(AnimalDto.SearchReq req) {
|
||||||
|
QAnimalEntity animal = QAnimalEntity.animalEntity;
|
||||||
|
|
||||||
|
Pageable pageable = req.toPageable();
|
||||||
|
|
||||||
|
List<AnimalEntity> content =
|
||||||
|
queryFactory
|
||||||
|
.selectFrom(animal)
|
||||||
|
.where(
|
||||||
|
animal.isDeleted.eq(false),
|
||||||
|
eqCategory(animal, req.getCategory()),
|
||||||
|
eqSpecies(animal, req.getSpecies()),
|
||||||
|
likeName(animal, req.getName()))
|
||||||
|
.offset(pageable.getOffset())
|
||||||
|
.limit(pageable.getPageSize())
|
||||||
|
.orderBy(animal.createdDate.desc())
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
// count 쿼리
|
||||||
|
Long total =
|
||||||
|
queryFactory
|
||||||
|
.select(animal.count())
|
||||||
|
.from(animal)
|
||||||
|
.where(
|
||||||
|
animal.isDeleted.eq(false),
|
||||||
|
eqCategory(animal, req.getCategory()),
|
||||||
|
eqSpecies(animal, req.getSpecies()),
|
||||||
|
likeName(animal, req.getName()))
|
||||||
|
.fetchOne();
|
||||||
|
|
||||||
|
return new PageImpl<>(content, pageable, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression likeName(QAnimalEntity animal, String nameStr) {
|
||||||
|
if (nameStr == null || nameStr.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return animal.name.containsIgnoreCase(nameStr.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression eqCategory(QAnimalEntity animal, Category category) {
|
||||||
|
if (category == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return animal.category.eq(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression eqSpecies(QAnimalEntity animal, Species species) {
|
||||||
|
if (species == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return animal.species.eq(species);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ZooRepository extends JpaRepository<ZooEntity, Long>, ZooRepositoryCustom {}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
public interface ZooRepositoryCustom {
|
||||||
|
|
||||||
|
Page<ZooEntity> listZoo(ZooDto.SearchReq searchReq);
|
||||||
|
|
||||||
|
Optional<ZooEntity> getZooByUuid(String uuid);
|
||||||
|
|
||||||
|
Optional<ZooEntity> getZooByUid(Long uid);
|
||||||
|
|
||||||
|
Long countActiveAnimals(Long zooId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 여러 Zoo의 활성 동물 수를 한 번에 조회 (N+1 문제 해결)
|
||||||
|
*
|
||||||
|
* @param zooIds Zoo ID 목록
|
||||||
|
* @return Map<Zoo ID, 활성 동물 수>
|
||||||
|
*/
|
||||||
|
Map<Long, Long> countActiveAnimalsByZooIds(List<Long> zooIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.repository;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.QAnimalEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.QZooEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
|
||||||
|
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||||
|
import com.querydsl.jpa.impl.JPAQuery;
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ZooRepositoryImpl implements ZooRepositoryCustom {
|
||||||
|
|
||||||
|
private final JPAQueryFactory queryFactory;
|
||||||
|
private final QZooEntity qZoo = QZooEntity.zooEntity;
|
||||||
|
private final QAnimalEntity qAnimal = QAnimalEntity.animalEntity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<ZooEntity> listZoo(ZooDto.SearchReq searchReq) {
|
||||||
|
Pageable pageable = searchReq.toPageable();
|
||||||
|
|
||||||
|
JPAQuery<ZooEntity> query =
|
||||||
|
queryFactory
|
||||||
|
.selectFrom(qZoo)
|
||||||
|
.where(
|
||||||
|
qZoo.isDeleted.eq(false),
|
||||||
|
nameContains(searchReq.getName()),
|
||||||
|
locationContains(searchReq.getLocation()));
|
||||||
|
|
||||||
|
long total = query.fetchCount();
|
||||||
|
|
||||||
|
List<ZooEntity> content =
|
||||||
|
query
|
||||||
|
.offset(pageable.getOffset())
|
||||||
|
.limit(pageable.getPageSize())
|
||||||
|
.orderBy(qZoo.createdDate.desc())
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
return new PageImpl<>(content, pageable, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ZooEntity> getZooByUuid(String uuid) {
|
||||||
|
return Optional.ofNullable(
|
||||||
|
queryFactory
|
||||||
|
.selectFrom(qZoo)
|
||||||
|
.where(qZoo.uuid.eq(UUID.fromString(uuid)), qZoo.isDeleted.eq(false))
|
||||||
|
.fetchOne());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ZooEntity> getZooByUid(Long uid) {
|
||||||
|
return Optional.ofNullable(
|
||||||
|
queryFactory.selectFrom(qZoo).where(qZoo.uid.eq(uid), qZoo.isDeleted.eq(false)).fetchOne());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long countActiveAnimals(Long zooId) {
|
||||||
|
Long count =
|
||||||
|
queryFactory
|
||||||
|
.select(qAnimal.count())
|
||||||
|
.from(qAnimal)
|
||||||
|
.where(qAnimal.zoo.uid.eq(zooId), qAnimal.isDeleted.eq(false))
|
||||||
|
.fetchOne();
|
||||||
|
|
||||||
|
return count != null ? count : 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<Long, Long> countActiveAnimalsByZooIds(List<Long> zooIds) {
|
||||||
|
if (zooIds == null || zooIds.isEmpty()) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryDSL group by로 한 번에 조회
|
||||||
|
List<com.querydsl.core.Tuple> results =
|
||||||
|
queryFactory
|
||||||
|
.select(qAnimal.zoo.uid, qAnimal.count())
|
||||||
|
.from(qAnimal)
|
||||||
|
.where(qAnimal.zoo.uid.in(zooIds), qAnimal.isDeleted.eq(false))
|
||||||
|
.groupBy(qAnimal.zoo.uid)
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
// Map으로 변환
|
||||||
|
Map<Long, Long> countMap = new HashMap<>();
|
||||||
|
for (com.querydsl.core.Tuple tuple : results) {
|
||||||
|
Long zooId = tuple.get(qAnimal.zoo.uid);
|
||||||
|
Long count = tuple.get(qAnimal.count());
|
||||||
|
if (zooId != null && count != null) {
|
||||||
|
countMap.put(zooId, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조회되지 않은 Zoo는 0으로 설정
|
||||||
|
for (Long zooId : zooIds) {
|
||||||
|
countMap.putIfAbsent(zooId, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
return countMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression nameContains(String name) {
|
||||||
|
return name != null && !name.isEmpty() ? qZoo.name.contains(name) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BooleanExpression locationContains(String location) {
|
||||||
|
return location != null && !location.isEmpty() ? qZoo.location.contains(location) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Basic;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Category;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto.Species;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.service.AnimalService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Tag(name = "Animal", description = "동물 관리 API")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping({"/api/animals", "/v1/api/animals"})
|
||||||
|
public class AnimalApiController {
|
||||||
|
|
||||||
|
private final AnimalService animalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 생성
|
||||||
|
*
|
||||||
|
* @param req 동물 생성 요청
|
||||||
|
* @return 생성된 동물 정보
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물 생성", description = "새로운 동물 정보를 등록합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "201",
|
||||||
|
description = "동물 생성 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = AnimalDto.Basic.class))),
|
||||||
|
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponseDto<Basic> createAnimal(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "동물 생성 요청 정보",
|
||||||
|
required = true,
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = AnimalDto.AddReq.class)))
|
||||||
|
@RequestBody
|
||||||
|
AnimalDto.AddReq req) {
|
||||||
|
AnimalDto.Basic created = animalService.createAnimal(req);
|
||||||
|
return ApiResponseDto.createOK(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID로 동물 조회
|
||||||
|
*
|
||||||
|
* @param uuid 동물 UUID
|
||||||
|
* @return 동물 정보
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물 단건 조회", description = "UUID로 특정 동물의 상세 정보를 조회합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "조회 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = AnimalDto.Basic.class))),
|
||||||
|
@ApiResponse(responseCode = "404", description = "동물을 찾을 수 없음", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@GetMapping("/{uuid}")
|
||||||
|
public ApiResponseDto<AnimalDto.Basic> getAnimal(
|
||||||
|
@Parameter(
|
||||||
|
description = "조회할 동물의 UUID",
|
||||||
|
required = true,
|
||||||
|
example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
|
@PathVariable
|
||||||
|
String uuid) {
|
||||||
|
Long id = animalService.getAnimalByUuid(uuid);
|
||||||
|
AnimalDto.Basic animal = animalService.getAnimal(id);
|
||||||
|
return ApiResponseDto.ok(animal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID로 동물 삭제 (논리 삭제)
|
||||||
|
*
|
||||||
|
* @param uuid 동물 UUID
|
||||||
|
* @return 삭제 성공 메시지
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물 삭제", description = "UUID로 특정 동물을 삭제합니다 (논리 삭제).")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(responseCode = "204", description = "삭제 성공", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "404", description = "동물을 찾을 수 없음", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@DeleteMapping("/{uuid}")
|
||||||
|
public ApiResponseDto<String> deleteAnimal(
|
||||||
|
@Parameter(
|
||||||
|
description = "삭제할 동물의 UUID",
|
||||||
|
required = true,
|
||||||
|
example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
|
@PathVariable
|
||||||
|
String uuid) {
|
||||||
|
Long id = animalService.getAnimalByUuid(uuid);
|
||||||
|
animalService.deleteAnimal(id);
|
||||||
|
return ApiResponseDto.deleteOk(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 검색 (페이징)
|
||||||
|
*
|
||||||
|
* @param name 동물 이름 (선택)
|
||||||
|
* @param category 서식지 타입 (선택)
|
||||||
|
* @param species 동물종 (선택) 개, 고양이등.
|
||||||
|
* @param page 페이지 번호 (기본값: 0)
|
||||||
|
* @param size 페이지 크기 (기본값: 20)
|
||||||
|
* @param sort 정렬 조건 (예: "name,asc")
|
||||||
|
* @return 페이징 처리된 동물 목록
|
||||||
|
*/
|
||||||
|
@Operation(
|
||||||
|
summary = "동물 검색",
|
||||||
|
description =
|
||||||
|
"다양한 조건으로 동물을 검색하고 페이징된 결과를 반환합니다. 모든 검색 조건은 선택적이며, 조건 없이 호출하면 전체 동물 목록을 반환합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "검색 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = Page.class))),
|
||||||
|
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponseDto<Page<AnimalDto.Basic>> searchAnimals(
|
||||||
|
@Parameter(description = "동물 이름 (부분 일치 검색)", example = "호랑이") @RequestParam(required = false)
|
||||||
|
String name,
|
||||||
|
@Parameter(description = "서식지 카테고리", example = "MAMMAL") @RequestParam(required = false)
|
||||||
|
Category category,
|
||||||
|
@Parameter(description = "동물 종", example = "TIGER") @RequestParam(required = false)
|
||||||
|
Species species,
|
||||||
|
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
|
||||||
|
int page,
|
||||||
|
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
|
||||||
|
int size,
|
||||||
|
@Parameter(description = "정렬 조건 (형식: 필드명,방향)", example = "name,asc")
|
||||||
|
@RequestParam(required = false)
|
||||||
|
String sort) {
|
||||||
|
AnimalDto.SearchReq searchReq =
|
||||||
|
new AnimalDto.SearchReq(name, category, species, page, size, sort);
|
||||||
|
Page<AnimalDto.Basic> animals = animalService.search(searchReq);
|
||||||
|
return ApiResponseDto.ok(animals);
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/main/java/com/kamco/cd/kamcoback/zoo/ZooApiController.java
Normal file
169
src/main/java/com/kamco/cd/kamcoback/zoo/ZooApiController.java
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto.Detail;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.service.ZooService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Tag(name = "Zoo", description = "동물원 관리 API")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping({"/api/zoos", "/v1/api/zoos"})
|
||||||
|
public class ZooApiController {
|
||||||
|
|
||||||
|
private final ZooService zooService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 생성
|
||||||
|
*
|
||||||
|
* @param req 동물원 생성 요청
|
||||||
|
* @return 생성된 동물원 정보 (동물 개수 포함)
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물원 생성", description = "새로운 동물원 정보를 등록합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "201",
|
||||||
|
description = "동물원 생성 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = ZooDto.AddReq.class))),
|
||||||
|
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponseDto<Detail> createZoo(
|
||||||
|
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||||
|
description = "동물원 생성 요청 정보",
|
||||||
|
required = true,
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = ZooDto.AddReq.class)))
|
||||||
|
@RequestBody
|
||||||
|
ZooDto.AddReq req) {
|
||||||
|
ZooDto.Detail created = zooService.createZoo(req);
|
||||||
|
return ApiResponseDto.createOK(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID로 동물원 조회
|
||||||
|
*
|
||||||
|
* @param uuid 동물원 UUID
|
||||||
|
* @return 동물원 정보 (현재 동물 개수 포함)
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물원 단건 조회", description = "UUID로 특정 동물원의 상세 정보를 조회합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "조회 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = ZooDto.Detail.class))),
|
||||||
|
@ApiResponse(responseCode = "404", description = "동물원을 찾을 수 없음", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@GetMapping("/{uuid}")
|
||||||
|
public ApiResponseDto<ZooDto.Detail> getZoo(
|
||||||
|
@Parameter(
|
||||||
|
description = "조회할 동물원의 UUID",
|
||||||
|
required = true,
|
||||||
|
example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
|
@PathVariable
|
||||||
|
String uuid) {
|
||||||
|
Long id = zooService.getZooByUuid(uuid);
|
||||||
|
ZooDto.Detail zoo = zooService.getZoo(id);
|
||||||
|
return ApiResponseDto.ok(zoo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID로 동물원 삭제 (논리 삭제)
|
||||||
|
*
|
||||||
|
* @param uuid 동물원 UUID
|
||||||
|
* @return 삭제 성공 메시지
|
||||||
|
*/
|
||||||
|
@Operation(summary = "동물원 삭제", description = "UUID로 특정 동물원을 삭제합니다 (논리 삭제).")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(responseCode = "204", description = "삭제 성공", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "404", description = "동물원을 찾을 수 없음", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@DeleteMapping("/{uuid}")
|
||||||
|
public ApiResponseDto<String> deleteZoo(
|
||||||
|
@Parameter(
|
||||||
|
description = "삭제할 동물원의 UUID",
|
||||||
|
required = true,
|
||||||
|
example = "550e8400-e29b-41d4-a716-446655440000")
|
||||||
|
@PathVariable
|
||||||
|
String uuid) {
|
||||||
|
Long id = zooService.getZooByUuid(uuid);
|
||||||
|
zooService.deleteZoo(id);
|
||||||
|
return ApiResponseDto.deleteOk(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 검색 (페이징)
|
||||||
|
*
|
||||||
|
* @param name 동물원 이름 (선택)
|
||||||
|
* @param location 위치 (선택)
|
||||||
|
* @param page 페이지 번호 (기본값: 0)
|
||||||
|
* @param size 페이지 크기 (기본값: 20)
|
||||||
|
* @param sort 정렬 조건 (예: "name,asc")
|
||||||
|
* @return 페이징 처리된 동물원 목록 (각 동물원의 현재 동물 개수 포함)
|
||||||
|
*/
|
||||||
|
@Operation(
|
||||||
|
summary = "동물원 검색",
|
||||||
|
description =
|
||||||
|
"다양한 조건으로 동물원을 검색하고 페이징된 결과를 반환합니다. 모든 검색 조건은 선택적이며, 조건 없이 호출하면 전체 동물원 목록을 반환합니다.")
|
||||||
|
@ApiResponses(
|
||||||
|
value = {
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "검색 성공",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = Page.class))),
|
||||||
|
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||||
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
|
})
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponseDto<Page<ZooDto.Detail>> searchZoos(
|
||||||
|
@Parameter(description = "동물원 이름 (부분 일치 검색)", example = "서울동물원")
|
||||||
|
@RequestParam(required = false)
|
||||||
|
String name,
|
||||||
|
@Parameter(description = "위치", example = "서울") @RequestParam(required = false)
|
||||||
|
String location,
|
||||||
|
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
|
||||||
|
int page,
|
||||||
|
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
|
||||||
|
int size,
|
||||||
|
@Parameter(description = "정렬 조건 (형식: 필드명,방향)", example = "name,asc")
|
||||||
|
@RequestParam(required = false)
|
||||||
|
String sort) {
|
||||||
|
ZooDto.SearchReq searchReq = new ZooDto.SearchReq(name, location, page, size, sort);
|
||||||
|
Page<ZooDto.Detail> zoos = zooService.search(searchReq);
|
||||||
|
return ApiResponseDto.ok(zoos);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/main/java/com/kamco/cd/kamcoback/zoo/dto/AnimalDto.java
Normal file
128
src/main/java/com/kamco/cd/kamcoback/zoo/dto/AnimalDto.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.kamco.cd.kamcoback.config.enums.EnumType;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
|
public class AnimalDto {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class AddReq {
|
||||||
|
|
||||||
|
private Category category;
|
||||||
|
private Species species;
|
||||||
|
private String name;
|
||||||
|
private Long zooId; // 동물원 ID (선택)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Basic {
|
||||||
|
|
||||||
|
@JsonIgnore private Long id;
|
||||||
|
private String uuid;
|
||||||
|
private Category category;
|
||||||
|
private Species species;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
timezone = "Asia/Seoul")
|
||||||
|
private ZonedDateTime createdDate;
|
||||||
|
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
timezone = "Asia/Seoul")
|
||||||
|
private ZonedDateTime modifiedDate;
|
||||||
|
|
||||||
|
public Basic(
|
||||||
|
Long id,
|
||||||
|
String uuid,
|
||||||
|
String name,
|
||||||
|
Category category,
|
||||||
|
Species species,
|
||||||
|
ZonedDateTime createdDate,
|
||||||
|
ZonedDateTime modifiedDate) {
|
||||||
|
this.id = id;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = name;
|
||||||
|
this.category = category;
|
||||||
|
this.species = species;
|
||||||
|
this.createdDate = createdDate;
|
||||||
|
this.modifiedDate = modifiedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum Category implements EnumType {
|
||||||
|
// @formatter:off
|
||||||
|
MAMMALS("100", "포유류"), // 땅에 사는 동물
|
||||||
|
BIRDS("200", "조류"), // 하늘을 나는 동물
|
||||||
|
FISH("300", "어류"),
|
||||||
|
AMPHIBIANS("400", "양서류"),
|
||||||
|
REPTILES("500", "파충류"),
|
||||||
|
INSECTS("500", "곤충"),
|
||||||
|
INVERTEBRATES("500", "무척추동물"),
|
||||||
|
;
|
||||||
|
// @formatter:on
|
||||||
|
private final String id;
|
||||||
|
private final String text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum Species implements EnumType {
|
||||||
|
// @formatter:off
|
||||||
|
DOG("101", "개"),
|
||||||
|
CAT("102", "강아지"),
|
||||||
|
DOVE("201", "비둘기"),
|
||||||
|
EAGLE("202", "독수리"),
|
||||||
|
SALMON("301", "연어"),
|
||||||
|
TUNA("302", "참치"),
|
||||||
|
;
|
||||||
|
// @formatter:on
|
||||||
|
private final String id;
|
||||||
|
private final String text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class SearchReq {
|
||||||
|
|
||||||
|
// 검색 조건
|
||||||
|
private String name;
|
||||||
|
private Category category;
|
||||||
|
private Species species;
|
||||||
|
|
||||||
|
// 페이징 파라미터
|
||||||
|
private int page = 0;
|
||||||
|
private int size = 20;
|
||||||
|
private String sort;
|
||||||
|
|
||||||
|
public Pageable toPageable() {
|
||||||
|
if (sort != null && !sort.isEmpty()) {
|
||||||
|
String[] sortParams = sort.split(",");
|
||||||
|
String property = sortParams[0];
|
||||||
|
Sort.Direction direction =
|
||||||
|
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||||
|
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||||
|
}
|
||||||
|
return PageRequest.of(page, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/main/java/com/kamco/cd/kamcoback/zoo/dto/ZooDto.java
Normal file
111
src/main/java/com/kamco/cd/kamcoback/zoo/dto/ZooDto.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
|
public class ZooDto {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class AddReq {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String location;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Basic {
|
||||||
|
|
||||||
|
@JsonIgnore private Long id;
|
||||||
|
private String uuid;
|
||||||
|
private String name;
|
||||||
|
private String location;
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
timezone = "Asia/Seoul")
|
||||||
|
private ZonedDateTime createdDate;
|
||||||
|
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||||
|
timezone = "Asia/Seoul")
|
||||||
|
private ZonedDateTime modifiedDate;
|
||||||
|
|
||||||
|
public Basic(
|
||||||
|
Long id,
|
||||||
|
String uuid,
|
||||||
|
String name,
|
||||||
|
String location,
|
||||||
|
String description,
|
||||||
|
ZonedDateTime createdDate,
|
||||||
|
ZonedDateTime modifiedDate) {
|
||||||
|
this.id = id;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.name = name;
|
||||||
|
this.location = location;
|
||||||
|
this.description = description;
|
||||||
|
this.createdDate = createdDate;
|
||||||
|
this.modifiedDate = modifiedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Detail extends Basic {
|
||||||
|
|
||||||
|
private Long activeAnimalCount;
|
||||||
|
|
||||||
|
public Detail(
|
||||||
|
Long id,
|
||||||
|
String uuid,
|
||||||
|
String name,
|
||||||
|
String location,
|
||||||
|
String description,
|
||||||
|
ZonedDateTime createdDate,
|
||||||
|
ZonedDateTime modifiedDate,
|
||||||
|
Long activeAnimalCount) {
|
||||||
|
super(id, uuid, name, location, description, createdDate, modifiedDate);
|
||||||
|
this.activeAnimalCount = activeAnimalCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class SearchReq {
|
||||||
|
|
||||||
|
// 검색 조건
|
||||||
|
private String name;
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
// 페이징 파라미터
|
||||||
|
private int page = 0;
|
||||||
|
private int size = 20;
|
||||||
|
private String sort;
|
||||||
|
|
||||||
|
public Pageable toPageable() {
|
||||||
|
if (sort != null && !sort.isEmpty()) {
|
||||||
|
String[] sortParams = sort.split(",");
|
||||||
|
String property = sortParams[0];
|
||||||
|
Sort.Direction direction =
|
||||||
|
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
|
||||||
|
return PageRequest.of(page, size, Sort.by(direction, property));
|
||||||
|
}
|
||||||
|
return PageRequest.of(page, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo.service;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.core.AnimalCoreService;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AnimalService {
|
||||||
|
|
||||||
|
private final AnimalCoreService zooCoreService;
|
||||||
|
|
||||||
|
// 동물의 UUID로 id조회
|
||||||
|
public Long getAnimalByUuid(String uuid) {
|
||||||
|
return zooCoreService.getDataByUuid(uuid).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 생성
|
||||||
|
*
|
||||||
|
* @param req 동물 생성 요청
|
||||||
|
* @return 생성된 동물 정보
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AnimalDto.Basic createAnimal(AnimalDto.AddReq req) {
|
||||||
|
return zooCoreService.create(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 삭제 (논리 삭제)
|
||||||
|
*
|
||||||
|
* @param id 동물 ID
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteAnimal(Long id) {
|
||||||
|
zooCoreService.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 단건 조회
|
||||||
|
*
|
||||||
|
* @param id 동물 ID
|
||||||
|
* @return 동물 정보
|
||||||
|
*/
|
||||||
|
public AnimalDto.Basic getAnimal(Long id) {
|
||||||
|
return zooCoreService.getOneById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물 검색 (페이징)
|
||||||
|
*
|
||||||
|
* @param searchReq 검색 조건
|
||||||
|
* @return 페이징 처리된 동물 목록
|
||||||
|
*/
|
||||||
|
public Page<AnimalDto.Basic> search(AnimalDto.SearchReq searchReq) {
|
||||||
|
return zooCoreService.search(searchReq);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.kamco.cd.kamcoback.zoo.service;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.postgres.core.ZooCoreService;
|
||||||
|
import com.kamco.cd.kamcoback.zoo.dto.ZooDto;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ZooService {
|
||||||
|
|
||||||
|
private final ZooCoreService zooCoreService;
|
||||||
|
|
||||||
|
// 동물원의 UUID로 id조회
|
||||||
|
public Long getZooByUuid(String uuid) {
|
||||||
|
return zooCoreService.getDataByUuid(uuid).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 생성
|
||||||
|
*
|
||||||
|
* @param req 동물원 생성 요청
|
||||||
|
* @return 생성된 동물원 정보 (동물 개수 포함)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ZooDto.Detail createZoo(ZooDto.AddReq req) {
|
||||||
|
return zooCoreService.create(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 삭제 (논리 삭제)
|
||||||
|
*
|
||||||
|
* @param id 동물원 ID
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteZoo(Long id) {
|
||||||
|
zooCoreService.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 단건 조회
|
||||||
|
*
|
||||||
|
* @param id 동물원 ID
|
||||||
|
* @return 동물원 정보 (동물 개수 포함)
|
||||||
|
*/
|
||||||
|
public ZooDto.Detail getZoo(Long id) {
|
||||||
|
return zooCoreService.getOneById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 동물원 검색 (페이징)
|
||||||
|
*
|
||||||
|
* @param searchReq 검색 조건
|
||||||
|
* @return 페이징 처리된 동물원 목록 (각 동물원의 동물 개수 포함)
|
||||||
|
*/
|
||||||
|
public Page<ZooDto.Detail> search(ZooDto.SearchReq searchReq) {
|
||||||
|
return zooCoreService.search(searchReq);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,3 +23,5 @@ spring:
|
|||||||
maximum-pool-size: 20
|
maximum-pool-size: 20
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ spring:
|
|||||||
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://10.100.0.10:25432/temp
|
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
|
||||||
username: temp
|
username: kamco_cds
|
||||||
password: temp123!
|
password: kamco_cds_Q!W@E#R$
|
||||||
hikari:
|
hikari:
|
||||||
minimum-idle: 1
|
minimum-idle: 1
|
||||||
maximum-pool-size: 5
|
maximum-pool-size: 5
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
30
src/main/resources/db/migration/V1__Create_Animal_Table.sql
Normal file
30
src/main/resources/db/migration/V1__Create_Animal_Table.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-- animal 테이블 생성
|
||||||
|
CREATE TABLE tb_animal
|
||||||
|
(
|
||||||
|
uid BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL UNIQUE,
|
||||||
|
category VARCHAR(50),
|
||||||
|
species VARCHAR(100),
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_date TIMESTAMPTZ NOT NULL,
|
||||||
|
modified_date TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 인덱스 생성
|
||||||
|
CREATE INDEX idx_animal_uuid ON tb_animal (uuid);
|
||||||
|
CREATE INDEX idx_animal_category ON tb_animal (category);
|
||||||
|
CREATE INDEX idx_animal_species ON tb_animal (species);
|
||||||
|
CREATE INDEX idx_animal_name ON tb_animal (name);
|
||||||
|
CREATE INDEX idx_animal_is_deleted ON tb_animal (is_deleted);
|
||||||
|
|
||||||
|
-- 주석 추가
|
||||||
|
COMMENT ON TABLE tb_animal IS '동물원 동물 정보';
|
||||||
|
COMMENT ON COLUMN tb_animal.uid IS '고유 식별자 (PK)';
|
||||||
|
COMMENT ON COLUMN tb_animal.uuid IS 'UUID (Unique)';
|
||||||
|
COMMENT ON COLUMN tb_animal.category IS '구분 (MAMMALS, BIRDS, FISH,AMPHIBIANS,REPTILES,INSECTS, INVERTEBRATES )';
|
||||||
|
COMMENT ON COLUMN tb_animal.species IS '동물 종';
|
||||||
|
COMMENT ON COLUMN tb_animal.name IS '동물 이름';
|
||||||
|
COMMENT ON COLUMN tb_animal.is_deleted IS '삭제 여부';
|
||||||
|
COMMENT ON COLUMN tb_animal.created_date IS '생성일시';
|
||||||
|
COMMENT ON COLUMN tb_animal.modified_date IS '수정일시';
|
||||||
28
src/main/resources/db/migration/V2__Create_Zoo_Table.sql
Normal file
28
src/main/resources/db/migration/V2__Create_Zoo_Table.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- zoo 테이블 생성
|
||||||
|
CREATE TABLE tb_zoo
|
||||||
|
(
|
||||||
|
uid BIGSERIAL PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL UNIQUE,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
location VARCHAR(300),
|
||||||
|
description TEXT,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_date TIMESTAMPTZ NOT NULL,
|
||||||
|
modified_date TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 인덱스 생성
|
||||||
|
CREATE INDEX idx_zoo_uuid ON tb_zoo (uuid);
|
||||||
|
CREATE INDEX idx_zoo_name ON tb_zoo (name);
|
||||||
|
CREATE INDEX idx_zoo_is_deleted ON tb_zoo (is_deleted);
|
||||||
|
|
||||||
|
-- 주석 추가
|
||||||
|
COMMENT ON TABLE tb_zoo IS '동물원 정보';
|
||||||
|
COMMENT ON COLUMN tb_zoo.uid IS '고유 식별자 (PK)';
|
||||||
|
COMMENT ON COLUMN tb_zoo.uuid IS 'UUID (Unique)';
|
||||||
|
COMMENT ON COLUMN tb_zoo.name IS '동물원 이름';
|
||||||
|
COMMENT ON COLUMN tb_zoo.location IS '위치';
|
||||||
|
COMMENT ON COLUMN tb_zoo.description IS '설명';
|
||||||
|
COMMENT ON COLUMN tb_zoo.is_deleted IS '삭제 여부';
|
||||||
|
COMMENT ON COLUMN tb_zoo.created_date IS '생성일시';
|
||||||
|
COMMENT ON COLUMN tb_zoo.modified_date IS '수정일시';
|
||||||
16
src/main/resources/db/migration/V3__Add_Zoo_Id_To_Animal.sql
Normal file
16
src/main/resources/db/migration/V3__Add_Zoo_Id_To_Animal.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- animal 테이블에 zoo_id 컬럼 추가
|
||||||
|
ALTER TABLE tb_animal
|
||||||
|
ADD COLUMN zoo_id BIGINT;
|
||||||
|
|
||||||
|
-- zoo_id에 대한 외래 키 제약조건 추가
|
||||||
|
ALTER TABLE tb_animal
|
||||||
|
ADD CONSTRAINT fk_animal_zoo
|
||||||
|
FOREIGN KEY (zoo_id)
|
||||||
|
REFERENCES tb_zoo (uid)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- zoo_id 인덱스 생성
|
||||||
|
CREATE INDEX idx_animal_zoo_id ON tb_animal (zoo_id);
|
||||||
|
|
||||||
|
-- 주석 추가
|
||||||
|
COMMENT ON COLUMN tb_animal.zoo_id IS '동물원 ID (FK)';
|
||||||
@@ -7,7 +7,5 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||||||
class KamcoBackApplicationTests {
|
class KamcoBackApplicationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user