diff --git a/.editorconfig b/.editorconfig
index a6d93a81..da27dc81 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,7 +1,7 @@
root = true
[*]
-indent_style = tab
+indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
@@ -9,4 +9,4 @@ trim_trailing_whitespace = true
insert_final_newline = true
[*.java]
-max_line_length = 100
+max_line_length = 180
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..f3c0d5e5
--- /dev/null
+++ b/CLAUDE.md
@@ -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`
diff --git a/CODE_STYLE_SETUP.md b/CODE_STYLE_SETUP.md
new file mode 100644
index 00000000..2c07ff5b
--- /dev/null
+++ b/CODE_STYLE_SETUP.md
@@ -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
diff --git a/README.md b/README.md
index 9b560246..5878b610 100644
--- a/README.md
+++ b/README.md
@@ -193,10 +193,6 @@ kamco-back/
│ │ │ │ └── StartupLogger.java # 시작 로그
│ │ │ ├── common/ # 공통 유틸리티
│ │ │ │ └── utils/geometry/ # GeoJSON 직렬화
-│ │ │ ├── domain/ # 도메인 엔티티 (예정)
-│ │ │ ├── repository/ # 리포지토리 (예정)
-│ │ │ ├── service/ # 비즈니스 로직 (예정)
-│ │ │ └── controller/ # REST 컨트롤러 (예정)
│ │ └── resources/
│ │ ├── application.yml # 기본 설정 + local/prod
│ │ └── application-dev.yml # 개발 환경 설정
diff --git a/build.gradle b/build.gradle
index 5c8b708c..7d1fc704 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
+ id 'com.diffplug.spotless' version '6.25.0'
}
group = 'com.kamco.cd'
@@ -48,6 +49,9 @@ dependencies {
// 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') {
@@ -58,3 +62,18 @@ tasks.named('test') {
bootJar {
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'
+}
diff --git a/intellij-java-google-style.xml b/intellij-java-google-style.xml
index f3a6743e..d63d7310 100644
--- a/intellij-java-google-style.xml
+++ b/intellij-java-google-style.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/settings.gradle b/settings.gradle
index 51bf7bfb..d4e5fa60 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,6 @@
+pluginManagement {
+ plugins {
+ id 'org.jetbrains.kotlin.jvm' version '2.2.20'
+ }
+}
rootProject.name = 'kamco-back'
diff --git a/src/main/java/com/kamco/cd/kamcoback/KamcoBackApplication.java b/src/main/java/com/kamco/cd/kamcoback/KamcoBackApplication.java
index 7a54c513..c03ed73d 100644
--- a/src/main/java/com/kamco/cd/kamcoback/KamcoBackApplication.java
+++ b/src/main/java/com/kamco/cd/kamcoback/KamcoBackApplication.java
@@ -9,5 +9,4 @@ public class KamcoBackApplication {
public static void main(String[] args) {
SpringApplication.run(KamcoBackApplication.class, args);
}
-
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/api/HelloApiController.java b/src/main/java/com/kamco/cd/kamcoback/common/api/HelloApiController.java
new file mode 100644
index 00000000..d8dcb066
--- /dev/null
+++ b/src/main/java/com/kamco/cd/kamcoback/common/api/HelloApiController.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/api/HelloDto.java b/src/main/java/com/kamco/cd/kamcoback/common/api/HelloDto.java
index 963b385e..f7668196 100644
--- a/src/main/java/com/kamco/cd/kamcoback/common/api/HelloDto.java
+++ b/src/main/java/com/kamco/cd/kamcoback/common/api/HelloDto.java
@@ -1,9 +1,36 @@
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 static class Req{
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class Req {
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;
+ }
+ }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/service/BaseCoreService.java b/src/main/java/com/kamco/cd/kamcoback/common/service/BaseCoreService.java
new file mode 100644
index 00000000..072e8081
--- /dev/null
+++ b/src/main/java/com/kamco/cd/kamcoback/common/service/BaseCoreService.java
@@ -0,0 +1,38 @@
+package com.kamco.cd.kamcoback.common.service;
+
+import org.springframework.data.domain.Page;
+
+/**
+ * Base Core Service Interface
+ *
+ *
CRUD operations를 정의하는 기본 서비스 인터페이스
+ *
+ * @param Entity 타입
+ * @param Entity의 ID 타입
+ * @param Search Request 타입
+ */
+public interface BaseCoreService {
+
+ /**
+ * ID로 엔티티를 삭제합니다.
+ *
+ * @param id 삭제할 엔티티의 ID
+ */
+ void remove(ID id);
+
+ /**
+ * ID로 단건 조회합니다.
+ *
+ * @param id 조회할 엔티티의 ID
+ * @return 조회된 엔티티
+ */
+ T getOneById(ID id);
+
+ /**
+ * 검색 조건과 페이징으로 조회합니다.
+ *
+ * @param searchReq 검색 조건
+ * @return 페이징 처리된 검색 결과
+ */
+ Page search(S searchReq);
+}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/service/HelloService.java b/src/main/java/com/kamco/cd/kamcoback/common/service/HelloService.java
new file mode 100644
index 00000000..a3b4b4f5
--- /dev/null
+++ b/src/main/java/com/kamco/cd/kamcoback/common/service/HelloService.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java
index f6eecdb2..ee14b8d5 100644
--- a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java
+++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometryDeserializer.java
@@ -4,34 +4,33 @@ import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import java.io.IOException;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonReader;
import org.springframework.util.StringUtils;
-import java.io.IOException;
-
public class GeometryDeserializer extends StdDeserializer {
- public GeometryDeserializer(Class targetType) {
- super(targetType);
+ public GeometryDeserializer(Class 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
- @SuppressWarnings("unchecked")
- @Override
- public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
- throws IOException, JacksonException {
- 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);
- }
+ try {
+ GeoJsonReader reader = new GeoJsonReader();
+ return (T) reader.read(json);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
}
+ }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometrySerializer.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometrySerializer.java
index b5433da6..733c36a0 100644
--- a/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometrySerializer.java
+++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/geometry/GeometrySerializer.java
@@ -3,29 +3,29 @@ package com.kamco.cd.kamcoback.common.utils.geometry;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
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.io.geojson.GeoJsonWriter;
-import java.io.IOException;
-import java.util.Objects;
-
public class GeometrySerializer extends StdSerializer {
- // TODO: test code
- public GeometrySerializer(Class targetType) {
- super(targetType);
- }
+ // TODO: test code
+ public GeometrySerializer(Class targetType) {
+ super(targetType);
+ }
- @Override
- public void serialize(T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
- throws IOException {
- if (Objects.nonNull(geometry)) {
- // default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
- GeoJsonWriter writer = new GeoJsonWriter(16);
- String json = writer.write(geometry);
- jsonGenerator.writeRawValue(json);
- } else {
- jsonGenerator.writeNull();
- }
+ @Override
+ public void serialize(
+ T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
+ throws IOException {
+ if (Objects.nonNull(geometry)) {
+ // default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
+ GeoJsonWriter writer = new GeoJsonWriter(16);
+ String json = writer.write(geometry);
+ jsonGenerator.writeRawValue(json);
+ } else {
+ jsonGenerator.writeNull();
}
+ }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/config/OpenApiConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/OpenApiConfig.java
new file mode 100644
index 00000000..afd6e60b
--- /dev/null
+++ b/src/main/java/com/kamco/cd/kamcoback/config/OpenApiConfig.java
@@ -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());
+ }
+}
diff --git a/src/main/java/com/kamco/cd/kamcoback/config/StartupLogger.java b/src/main/java/com/kamco/cd/kamcoback/config/StartupLogger.java
index 1de2b594..4df4c837 100644
--- a/src/main/java/com/kamco/cd/kamcoback/config/StartupLogger.java
+++ b/src/main/java/com/kamco/cd/kamcoback/config/StartupLogger.java
@@ -1,6 +1,5 @@
package com.kamco.cd.kamcoback.config;
-
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
@@ -15,85 +14,83 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class StartupLogger {
- private final Environment environment;
- private final DataSource dataSource;
+ private final Environment environment;
+ private final DataSource dataSource;
- @EventListener(ApplicationReadyEvent.class)
- public void logStartupInfo() {
- String[] activeProfiles = environment.getActiveProfiles();
- String profileInfo =
- activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
+ @EventListener(ApplicationReadyEvent.class)
+ public void logStartupInfo() {
+ String[] activeProfiles = environment.getActiveProfiles();
+ String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
- // Database connection information
- String dbUrl = environment.getProperty("spring.datasource.url");
- String dbUsername = environment.getProperty("spring.datasource.username");
- String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
+ // Database connection information
+ String dbUrl = environment.getProperty("spring.datasource.url");
+ String dbUsername = environment.getProperty("spring.datasource.username");
+ String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
- // HikariCP pool settings
- String poolInfo = "";
- if (dataSource instanceof HikariDataSource hikariDs) {
- poolInfo =
- String.format(
- """
- │ Pool Size : min=%d, max=%d
- │ Connection Timeout: %dms
- │ Idle Timeout : %dms
- │ Max Lifetime : %dms""",
- hikariDs.getMinimumIdle(),
- hikariDs.getMaximumPoolSize(),
- hikariDs.getConnectionTimeout(),
- hikariDs.getIdleTimeout(),
- hikariDs.getMaxLifetime());
- }
+ // HikariCP pool settings
+ String poolInfo = "";
+ if (dataSource instanceof HikariDataSource hikariDs) {
+ poolInfo =
+ String.format(
+ """
+ │ Pool Size : min=%d, max=%d
+ │ Connection Timeout: %dms
+ │ Idle Timeout : %dms
+ │ Max Lifetime : %dms""",
+ hikariDs.getMinimumIdle(),
+ hikariDs.getMaximumPoolSize(),
+ hikariDs.getConnectionTimeout(),
+ hikariDs.getIdleTimeout(),
+ hikariDs.getMaxLifetime());
+ }
- // JPA/Hibernate settings
- String showSql = environment.getProperty("spring.jpa.show-sql", "false");
- String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
- String batchSize =
- environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
- String batchFetchSize =
- environment.getProperty(
- "spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
+ // JPA/Hibernate settings
+ String showSql = environment.getProperty("spring.jpa.show-sql", "false");
+ String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
+ String batchSize =
+ environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
+ String batchFetchSize =
+ environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
- String startupMessage =
- String.format(
- """
+ String startupMessage =
+ String.format(
+ """
- ╔════════════════════════════════════════════════════════════════════════════════╗
- ║ 🚀 APPLICATION STARTUP INFORMATION ║
- ╠════════════════════════════════════════════════════════════════════════════════╣
- ║ PROFILE CONFIGURATION ║
- ╠────────────────────────────────────────────────────────────────────────────────╣
- │ Active Profile(s): %s
- ╠════════════════════════════════════════════════════════════════════════════════╣
- ║ DATABASE CONFIGURATION ║
- ╠────────────────────────────────────────────────────────────────────────────────╣
- │ Database URL : %s
- │ Username : %s
- │ Driver : %s
- ╠════════════════════════════════════════════════════════════════════════════════╣
- ║ HIKARICP CONNECTION POOL ║
- ╠────────────────────────────────────────────────────────────────────────────────╣
- %s
- ╠════════════════════════════════════════════════════════════════════════════════╣
- ║ JPA/HIBERNATE CONFIGURATION ║
- ╠────────────────────────────────────────────────────────────────────────────────╣
- │ Show SQL : %s
- │ DDL Auto : %s
- │ JDBC Batch Size : %s
- │ Fetch Batch Size : %s
- ╚════════════════════════════════════════════════════════════════════════════════╝
- """,
- profileInfo,
- dbUrl != null ? dbUrl : "N/A",
- dbUsername != null ? dbUsername : "N/A",
- dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
- poolInfo,
- showSql,
- ddlAuto,
- batchSize,
- batchFetchSize);
+ ╔════════════════════════════════════════════════════════════════════════════════╗
+ ║ 🚀 APPLICATION STARTUP INFORMATION ║
+ ╠════════════════════════════════════════════════════════════════════════════════╣
+ ║ PROFILE CONFIGURATION ║
+ ╠────────────────────────────────────────────────────────────────────────────────╣
+ │ Active Profile(s): %s
+ ╠════════════════════════════════════════════════════════════════════════════════╣
+ ║ DATABASE CONFIGURATION ║
+ ╠────────────────────────────────────────────────────────────────────────────────╣
+ │ Database URL : %s
+ │ Username : %s
+ │ Driver : %s
+ ╠════════════════════════════════════════════════════════════════════════════════╣
+ ║ HIKARICP CONNECTION POOL ║
+ ╠────────────────────────────────────────────────────────────────────────────────╣
+ %s
+ ╠════════════════════════════════════════════════════════════════════════════════╣
+ ║ JPA/HIBERNATE CONFIGURATION ║
+ ╠────────────────────────────────────────────────────────────────────────────────╣
+ │ Show SQL : %s
+ │ DDL Auto : %s
+ │ JDBC Batch Size : %s
+ │ Fetch Batch Size : %s
+ ╚════════════════════════════════════════════════════════════════════════════════╝
+ """,
+ profileInfo,
+ dbUrl != null ? dbUrl : "N/A",
+ dbUsername != null ? dbUsername : "N/A",
+ dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
+ poolInfo,
+ showSql,
+ ddlAuto,
+ batchSize,
+ batchFetchSize);
- log.info(startupMessage);
- }
+ log.info(startupMessage);
+ }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/config/WebConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/WebConfig.java
index 6671faba..1b8efc7a 100644
--- a/src/main/java/com/kamco/cd/kamcoback/config/WebConfig.java
+++ b/src/main/java/com/kamco/cd/kamcoback/config/WebConfig.java
@@ -14,20 +14,18 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class WebConfig {
- @Bean
- public ObjectMapper objectMapper() {
- SimpleModule module = new SimpleModule();
- module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
- module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
+ @Bean
+ public ObjectMapper objectMapper() {
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
+ module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
- module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
- module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
+ module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
+ module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
- module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
- module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
+ module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
+ module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
- return Jackson2ObjectMapperBuilder.json()
- .modulesToInstall(module)
- .build();
- }
+ return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
+ }
}
diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java
new file mode 100644
index 00000000..eb8cc764
--- /dev/null
+++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseAdvice.java
@@ -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
+ *
+ * createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
+ */
+@RestControllerAdvice
+public class ApiResponseAdvice implements ResponseBodyAdvice