Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
20 changes: 20 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
root = true

[*.{kt,kts}]
max_line_length=130
insert_final_newline = true

ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = unset

ktlint_code_style = INTELLIJ_IDEA
ktlint_standard_package-name = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_indent = disabled

[*Test.kt]
max_line_length = off
22 changes: 11 additions & 11 deletions .github/workflows/dev-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,34 @@ jobs:

- name: Create application-dev.yml
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.PROPERTIES_DEV }}" > ./src/main/resources/application-dev.yml
mkdir -p ./apps/juinjang-api/src/main/resources
echo "${{ secrets.PROPERTIES_DEV }}" > ./apps/juinjang-api/src/main/resources/application-dev.yml
shell: bash

- name: Create .p8
run: |
echo "${{ secrets.APPLE_AUTH }}" > ./src/main/resources/AUTHKEY_JUINJAG.p8
echo "${{ secrets.APPLE_AUTH }}" > ./apps/juinjang-api/src/main/resources/AUTHKEY_JUINJAG.p8
shell: bash

# APPLE IN_APP 결제 관련 프로세스 시작
- name: Create certs and keys directories
run: |
mkdir -p ./src/main/resources/keys
mkdir -p ./apps/juinjang-api/src/main/resources/keys

- name: Create IAP .p8 Key
run: |
echo "${{ secrets.APPLE_IAP_KEY }}" > ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8
echo "${{ secrets.APPLE_IAP_KEY }}" > ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8

# 키 파일 크기 확인 (내용은 로그에 출력하지 않음)
if [ -s "./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8" ]; then
echo "✅ IAP key file created: $(wc -c < ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8) bytes"
if [ -s "./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8" ]; then
echo "✅ IAP key file created: $(wc -c < ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8) bytes"
else
echo "❌ IAP key file is empty!"
exit 1
fi

# PEM 형식 확인
if grep -q "BEGIN PRIVATE KEY" ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8; then
if grep -q "BEGIN PRIVATE KEY" ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8; then
echo "✅ IAP key file appears to be in PEM format"
else
echo "⚠️ IAP key file may not be in PEM format"
Expand All @@ -72,7 +72,7 @@ jobs:
# APPLE IN_APP 결제 관련 프로세스 끝

- name: Build With Gradle
run: ./gradlew build -x test
run: ./gradlew :apps:juinjang-api:build -x test

- name: Login to Docker Hub
run: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ jobs:

- name: Create application-dev.yml
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.PROPERTIES_DEV }}" > ./src/main/resources/application-dev.yml
mkdir -p ./apps/juinjang-api/src/main/resources
echo "${{ secrets.PROPERTIES_DEV }}" > ./apps/juinjang-api/src/main/resources/application-dev.yml
shell: bash

- name: Build With Gradle
run: ./gradlew build -x test
run: ./gradlew :apps:juinjang-api:build -x test
18 changes: 9 additions & 9 deletions .github/workflows/prod-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,34 @@ jobs:

- name: Create application-prod.yml
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.PROPERTIES_PROD }}" > ./src/main/resources/application-prod.yml
mkdir -p ./apps/juinjang-api/src/main/resources
echo "${{ secrets.PROPERTIES_PROD }}" > ./apps/juinjang-api/src/main/resources/application-prod.yml
shell: bash

- name: Create .p8
run: |
echo "${{ secrets.APPLE_AUTH }}" > ./src/main/resources/AUTHKEY_JUINJAG.p8
echo "${{ secrets.APPLE_AUTH }}" > ./apps/juinjang-api/src/main/resources/AUTHKEY_JUINJAG.p8
shell: bash

# APPLE IN_APP 결제 관련 프로세스 시작
- name: Create certs and keys directories
run: |
mkdir -p ./src/main/resources/keys
mkdir -p ./apps/juinjang-api/src/main/resources/keys

- name: Create IAP .p8 Key
run: |
echo "${{ secrets.APPLE_IAP_KEY_PROD }}" > ./src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8
echo "${{ secrets.APPLE_IAP_KEY_PROD }}" > ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8

# 키 파일 크기 확인 (내용은 로그에 출력하지 않음)
if [ -s "./src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8" ]; then
echo "✅ IAP key file created: $(wc -c < ./src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8) bytes"
if [ -s "./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8" ]; then
echo "✅ IAP key file created: $(wc -c < ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8) bytes"
else
echo "❌ IAP key file is empty!"
exit 1
fi

# PEM 형식 확인
if grep -q "BEGIN PRIVATE KEY" ./src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8; then
if grep -q "BEGIN PRIVATE KEY" ./apps/juinjang-api/src/main/resources/keys/SubscriptionKey_62QGYSCKW2.p8; then
echo "✅ IAP key file appears to be in PEM format"
else
echo "⚠️ IAP key file may not be in PEM format"
Expand All @@ -72,7 +72,7 @@ jobs:
# APPLE IN_APP 결제 관련 프로세스 끝

- name: Build With Gradle
run: ./gradlew build -x test
run: ./gradlew :apps:juinjang-api:build -x test

- name: Login to Docker Hub
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
run: chmod +x gradlew

- name: Build With Gradle
run: ./gradlew build -x test
run: ./gradlew :apps:juinjang-api:build -x test
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ out/
/nbdist/
/.nb-gradle/

### Claude Code ###
.claude/

### Kotlin ###
.kotlin/

### VS Code ###
.vscode/
/application.properties
Expand Down
122 changes: 122 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**주인장 (Juinjang)** - 부동산 임장 관리 서비스의 Spring Boot 백엔드 애플리케이션

## Tech Stack

- **Language**: Java 17 (primary), Kotlin 2.0.20 (being introduced)
- **Framework**: Spring Boot 3.4.4
- **Build**: Gradle 8.5 (Kotlin DSL)
- **ORM**: Spring Data JPA + QueryDSL 5.0.0 (jakarta)
- **Database**: MySQL (production), H2 (test)
- **Cache**: Redis
- **Auth**: Spring Security + OAuth2 (Kakao, Apple) + JWT (jjwt 0.11.5)
- **Cloud**: AWS S3, Google Cloud Vision, Apple StoreKit
- **Monitoring**: Prometheus + Micrometer
- **Lint**: ktlint 12.1.2 (IntelliJ IDEA code style)

## Module Structure

멀티모듈 Gradle 프로젝트:

```
juinjang (root)
└── apps/
└── juinjang-api/ # API 애플리케이션 모듈
```

소스 코드 위치: `apps/juinjang-api/src/main/java/umc/th/juinjang/`

### Architecture (Layered)

```
api/ → Controllers, Request/Response DTOs, Service interfaces
auth/ → JWT filters, Security config, OAuth2
domain/ → Entities, Repositories, QueryDSL custom repos
common/ → Status codes, exceptions, validation, Redis config
config/ → Application configuration
event/ → Publisher/Subscriber (event-driven 패턴)
```

Kotlin 소스(`src/main/kotlin/`)에 새로운 계층 구조 도입 중:
```
interfaces/ → API 진입점
application/ → Facade (유스케이스 조합)
domain/ → 엔티티, 도메인 로직
infrastructure/ → 외부 연동
support/ → 공통 지원
```

## Common Commands

```bash
# 빌드 (테스트 제외)
./gradlew :apps:juinjang-api:build -x test

# 테스트 실행
./gradlew :apps:juinjang-api:test

# 단일 테스트 클래스 실행
./gradlew :apps:juinjang-api:test --tests "umc.th.juinjang.api.limjang.LimjangControllerTest"

# 클린 빌드
./gradlew clean :apps:juinjang-api:build

# 애플리케이션 실행 (dev 프로필)
./gradlew :apps:juinjang-api:bootRun --args='--spring.profiles.active=dev'

# QueryDSL Q클래스 생성
./gradlew :apps:juinjang-api:compileJava

# ktlint 검사
./gradlew :apps:juinjang-api:ktlintCheck

# ktlint 자동 포맷
./gradlew :apps:juinjang-api:ktlintFormat
```

## Configuration

- `application-dev.yml`, `application-prod.yml`: GitHub Secrets로 관리 (git에 포함되지 않음)
- `application-test.yml`: H2 인메모리 DB (MySQL 모드), 테스트용 더미 값 포함
- 환경별 logback 설정: `logback-dev.xml`, `logback-prod.xml`, `logback-local.xml`

## CI/CD

- **CI**: GitHub Actions — PR to `dev`/`prod` 시 빌드 검증
- **CD**: GitHub Actions — `dev`/`prod` push 시 Docker 빌드 → Docker Hub → EC2 배포
- **Docker Base Image**: `eclipse-temurin:17-jdk`
- **JAR 경로**: `apps/juinjang-api/build/libs/juinjang-api-0.0.1-SNAPSHOT.jar`

## Git Convention

- **Commit**: `[type : 작업내용 #이슈번호]` (예: `add : 이미지 파일 추가 #223`)
- **Branch**: `type/작업내용` (예: `fix/loginerror`)
- **PR Template**: `[type/#n]작업 내용`
- **Main branches**: `dev` (개발), `prod` (운영)

| Type | Description |
|------|-------------|
| add | 새로운 파일 추가 |
| feat | 새로운 기능 추가/수정 |
| fix | 버그 수정 |
| build | 빌드 관련 수정 |
| chore | 패키지 매니저, 기타 수정 |
| ci | CI 관련 설정 수정 |
| docs | 문서 수정 |
| style | 코드 스타일, 포매팅 |
| refactor | 코드 리팩터링 |
| test | 테스트 코드 |
| release | 버전 릴리즈 |
| remove | 코드/파일 제거 |

## Testing

- **Test Support**: `ControllerTestSupport` (MockMvc 기반), `IntegrationTestSupport` (통합 테스트)
- **Mocking**: SpringMockK, Mockito-Kotlin
- **Fixtures**: Instancio로 테스트 데이터 생성
- **Test Profile**: `application-test.yml` (H2, Redis localhost:6379)
9 changes: 3 additions & 6 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
FROM eclipse-temurin:17-jdk

ARG JAR_FILE=./build/libs/juinjang-0.0.1-SNAPSHOT.jar
ARG JAR_FILE=./apps/juinjang-api/build/libs/juinjang-api-0.0.1-SNAPSHOT.jar
ENV GOOGLE_APPLICATION_CREDENTIALS=/app/config/service-account-key.json
ENV TZ=Asia/Seoul
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/var/heapdumps/juinjang", "-Dspring.profiles.active=dev", "-jar", "/app.jar"]




ENTRYPOINT ["java", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/var/heapdumps/juinjang", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=dev", "-jar", "/app.jar"]
8 changes: 3 additions & 5 deletions Dockerfile-prod
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
FROM eclipse-temurin:17-jdk

ARG JAR_FILE=./build/libs/juinjang-0.0.1-SNAPSHOT.jar
ARG JAR_FILE=./apps/juinjang-api/build/libs/juinjang-api-0.0.1-SNAPSHOT.jar
ENV TZ=Asia/Seoul
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/var/heapdumps/juinjang", "-Dspring.profiles.active=prod", "-jar", "/app.jar"]



ENTRYPOINT ["java", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/var/heapdumps/juinjang", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=prod", "-jar", "/app.jar"]
58 changes: 58 additions & 0 deletions apps/juinjang-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
plugins {
id("org.jetbrains.kotlin.plugin.jpa")
}

dependencies {
// Modules
implementation(project(":supports:monitoring"))
implementation(project(":supports:logging"))
implementation(project(":modules:jpa"))
implementation(project(":modules:redis"))
testImplementation(testFixtures(project(":modules:jpa")))

// Lombok (코드 마이그레이션 전까지 유지)
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testCompileOnly("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")

// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-actuator")

// Spring Cloud
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")

// Spring Retry
implementation("org.springframework.retry:spring-retry")

// QueryDSL (이 모듈 자체 엔티티용 APT)
kapt("com.querydsl:querydsl-apt:${project.properties["queryDslVersion"]}:jakarta")

// JWT
implementation("io.jsonwebtoken:jjwt-api:${project.properties["jjwtVersion"]}")
runtimeOnly("io.jsonwebtoken:jjwt-impl:${project.properties["jjwtVersion"]}")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:${project.properties["jjwtVersion"]}")

// AWS S3
implementation("org.springframework.cloud:spring-cloud-starter-aws:${project.properties["springCloudAwsVersion"]}")

// Google Cloud Vision
implementation("com.google.cloud:google-cloud-vision:${project.properties["googleCloudVisionVersion"]}")

// Apple StoreKit
implementation("com.apple.itunes.storekit:app-store-server-library:${project.properties["appleStoreKitVersion"]}")

// Documentation
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

// Database
runtimeOnly("com.h2database:h2")

// Test
testImplementation("org.springframework.security:spring-security-test")
}
Loading