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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions .claude/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# API.md — REST API 계약 및 컨트롤러

## 목표 (Goal)

외부 클라이언트에 노출되는 검색·라우팅·관리자 REST API의 계약(URI, 파라미터, 응답 스키마)을 확정하고, 그에 대응하는 컨트롤러·DTO·예외 처리를 구현한다. 1차 프로토타입은 **인증 없이** 모든 엔드포인트를 노출한다(운영 전 Spring Security 도입 필수).

## 선행조건 (Prerequisites)

- [ROUTING.md](ROUTING.md) 완료: `RouteService.route(...)` 호출 가능.
- [SEARCH.md](SEARCH.md) 완료: `SearchService.autocomplete(...)`, `findById(...)` 호출 가능.
- [DATA_MODEL.md](DATA_MODEL.md) 완료: 도메인 엔티티 CRUD 가능.

## 변경/생성 파일 (Files to Change)

| 경로 | 종류 | 역할 |
|------|------|------|
| `../src/main/java/com/honggwart/map/search/api/SearchController.java` | 신규 | 자동완성·상세 조회 |
| `../src/main/java/com/honggwart/map/search/api/dto/*` | 신규 | 응답 DTO (SEARCH.md와 공유) |
| `../src/main/java/com/honggwart/map/routing/api/RouteController.java` | 신규 | 길찾기 |
| `../src/main/java/com/honggwart/map/routing/api/dto/*` | 신규 | RouteResponse, StepDto, PolylinePointDto 등 |
| `../src/main/java/com/honggwart/map/admin/api/AdminController.java` | 신규 | Place·Node·Edge CRUD + reload/reindex |
| `../src/main/java/com/honggwart/map/admin/api/dto/*` | 신규 | 관리자 요청·응답 DTO |
| `../src/main/java/com/honggwart/map/common/web/ApiExceptionHandler.java` | 신규 | `@RestControllerAdvice`로 400/404 매핑 |

## 구현 (Implementation)

### §1. 검색 API

#### `GET /api/search/autocomplete`

```
GET /api/search/autocomplete?q=T&limit=10
→ 200
{
"suggestions": [
{ "placeId": 101, "displayName": "공학관 501호", "kind": "ROOM", "category": "OFFICE" },
{ "placeId": 1, "displayName": "공학관", "kind": "BUILDING" }
]
}
```
Comment on lines +31 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 블록 언어 태그를 명시해 markdownlint(MD040) 경고를 제거해 주세요.

예: API 예시는 ```http, JSON 응답은 ```json, 엔드포인트 목록은 ```text로 지정하면 됩니다.

Also applies to: 49-63, 72-97, 134-142, 146-153, 157-160

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/API.md around lines 31 - 40, The markdown code fences in the API
examples lack language tags, triggering markdownlint MD040; update the example
block that shows "GET /api/search/autocomplete" to use explicit fenced languages
(use ```http for the request block and ```json for the response block), and
apply the same fix to all other API example/request/response/code fences
throughout the file so request examples use ```http, JSON responses use ```json,
and plain lists or endpoints use ```text.


- 파라미터: `q`(required, 1자 이상), `limit`(optional, 기본 10, 최대 50).
- 빈 q는 400.
- 응답 단위는 `place_id`. ES 쿼리 후 중복 제거하고 weight 순 정렬은 [SEARCH.md](SEARCH.md) §5에서 처리.
- 입력 정규화(NFC, 소문자, 공백 단일화)는 ES analyzer가 담당. 컨트롤러는 trim만.

#### `GET /api/places/{placeId}`

```
GET /api/places/101
→ 200
{
"placeId": 101,
"kind": "ROOM",
"displayName": "공학관 501호",
"category": "OFFICE",
"parentPlace": { "placeId": 1, "displayName": "공학관" },
"entrances": [
{ "label": "정문", "nodeId": 551, "isPrimary": true },
{ "label": "후문", "nodeId": 552, "isPrimary": false }
]
}
```

- 미존재 placeId는 404.
- 데이터 원천은 PostgreSQL (`SearchService.findById`).

### §2. 라우팅 API

#### `GET /api/route`

```
GET /api/route?fromPlaceId=101&toPlaceId=250&mode=INDOOR_PREFERRED
→ 200
{
"summary": {
"distanceMeters": 320.5,
"estimatedSeconds": 280,
"floorChanges": 1,
"mode": "INDOOR_PREFERRED"
},
"from": { "placeId": 101, "entranceLabel": "정문", "nodeId": 551 },
"to": { "placeId": 250, "entranceLabel": "후문", "nodeId": 902 },
"polyline": [
{ "lat": 37.0001, "lng": 127.0001, "floor": 1, "indoor": false }
],
"steps": [
{ "type": "DEPART", "instruction": "공학관 정문에서 출발합니다", "polylineRange": [0, 0] },
{ "type": "TURN_LEFT", "instruction": "좌회전 후 직진하세요", "polylineRange": [0, 12] },
{ "type": "ENTER_BUILDING", "instruction": "공학관 안으로 들어가세요", "polylineRange": [12, 13] },
{ "type": "FLOOR_CHANGE", "instruction": "엘리베이터로 5층까지 올라가세요",
"meta": { "fromFloor": 1, "toFloor": 5, "transport": "ELEVATOR" },
"polylineRange": [13, 14] },
{ "type": "ARRIVE", "instruction": "공학관 501호에 도착했습니다", "polylineRange": [14, 14] }
]
}
```

**파라미터**

- `fromPlaceId` (long, required)
- `toPlaceId` (long, required)
- `mode` (enum, optional, 기본 `SHORTEST`) — `SHORTEST` | `INDOOR_PREFERRED` | `STAIR_AVOIDANCE`

**필드 의미**

- `polyline`: 지도 렌더링용 전체 노드열. 곡선이면 모든 점이 포함됨.
- `steps[*].polylineRange = [startIndex, endIndex]`: 해당 step이 덮는 polyline 인덱스 구간. 곡선은 단일 step의 range 안에 흡수됨.
- `from.entranceLabel` / `to.entranceLabel`: 다중 입구 중 실제 사용된 입구의 label (RouteService가 [ROUTING.md](ROUTING.md) §4 다중 출발/도착 Dijkstra로 자동 선택).

**StepType enum**

`DEPART | TURN_LEFT | TURN_RIGHT | ENTER_BUILDING | EXIT_BUILDING | FLOOR_CHANGE | ARRIVE`

**FLOOR_CHANGE.meta**

```json
{ "fromFloor": 1, "toFloor": 5, "transport": "ELEVATOR" }
```

`transport`는 `STAIR` | `ELEVATOR` (해당 구간 Edge.edge_type에서 도출).

**에러**

- 미존재 placeId, 경로 없음(연결 안 됨) → 404 + 에러 메시지.
- 잘못된 mode → 400.

### §3. 관리자 API (1차: 인증 없음)

`/api/admin/**` 하위. **인증 미적용** — 1차 프로토타입 한정. 운영 전 Spring Security 도입 필수.
Comment on lines +128 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

무인증 관리자 API는 운영 환경 강제 차단 규칙이 필요합니다.

“운영 전 도입 필수” 문구만으로는 배포 실수를 막지 못합니다. prod 프로파일에서 /api/admin/** 비활성화(또는 앱 시작 실패) 같은 강제 가드를 계약에 추가해 주세요.

Also applies to: 168-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/API.md around lines 128 - 130, Add a runtime guard that prevents the
unauthenticated admin endpoints under "/api/admin/**" from being active when the
application runs with the production profile; implement a startup check that
reads the active Spring profile (e.g., "prod" or spring.profiles.active) and
either unregisters/disables the "/api/admin/**" endpoints or fails application
startup with a clear error if those routes are still enabled, so the
unauthenticated admin API cannot be deployed to prod by accident.


#### Place CRUD

```
POST /api/admin/places # Place 생성
PUT /api/admin/places/{id}
DELETE /api/admin/places/{id}
POST /api/admin/places/{id}/aliases
DELETE /api/admin/places/{id}/aliases/{aliasId}
POST /api/admin/places/{id}/entrances # PlaceEntrance 등록
DELETE /api/admin/places/{id}/entrances/{entranceId}
```

#### Node/Edge CRUD

```
POST /api/admin/nodes
PUT /api/admin/nodes/{id}
DELETE /api/admin/nodes/{id}
POST /api/admin/edges
PUT /api/admin/edges/{id}
DELETE /api/admin/edges/{id}
```

#### 운영 작업

```
POST /api/admin/graph/reload # GraphRegistry 수동 재빌드
POST /api/admin/index/reindex # ES 전체 재색인
```

#### 동기화 트리거

- Place·PlaceAlias·PlaceEntrance 변경 트랜잭션 커밋 후 → `PlacePersistedEvent` 발행 → [SEARCH.md](SEARCH.md) `PlaceIndexer`가 수신해 ES 색인 갱신.
- Node·Edge 변경 트랜잭션 커밋 후 → `GraphChangedEvent` 발행 → [ROUTING.md](ROUTING.md) `GraphRegistry.reload()` 호출.
- 이벤트 발행 책임은 `AdminController`(또는 그가 호출하는 서비스 계층)에 있다.

**경고**: 이 영역의 모든 엔드포인트는 1차 프로토타입 한정 인증 없음. 실제 사용자에게 공개 가능한 환경에서는 반드시 인증 미들웨어를 거쳐야 한다.

### §4. 공통 처리

- DTO에는 Bean Validation(`@NotNull`, `@Positive` 등)을 적용.
- `@RestControllerAdvice`로 `MethodArgumentNotValidException` → 400, `EntityNotFoundException` 등 → 404로 매핑.
- 응답 일관성: 모든 4xx/5xx는 `{ "error": { "code": "...", "message": "..." } }` 형태.
Comment on lines +172 to +174
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

쿼리 파라미터 검증 예외 매핑이 누락되어 400 계약이 깨질 수 있습니다.

현재 매핑 정의만으로는 q 누락/mode 타입 불일치 같은 GET 파라미터 오류가 400으로 일관 처리되지 않을 수 있습니다. MissingServletRequestParameterException, MethodArgumentTypeMismatchException, ConstraintViolationException도 400으로 명시해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/API.md around lines 172 - 174, Extend the existing
`@RestControllerAdvice` exception mapping that currently handles
MethodArgumentNotValidException to also handle
MissingServletRequestParameterException, MethodArgumentTypeMismatchException,
and ConstraintViolationException so GET query/param errors always return HTTP
400 with the standardized body {"error": {"code": "...", "message": "..."}};
update the global handler (the class that defines the
MethodArgumentNotValidException handler) to add specific `@ExceptionHandler`
methods (or a single handler accepting multiple exception types) that map those
three exceptions to a 400 response, normalize the error.code and error.message
fields to match the existing 4xx contract, and ensure
ConstraintViolationException translates constraint details into a readable
message for the response.


## 검증 (Verification)

### MockMvc 테스트 (단위·슬라이스)
- `SearchControllerTest`: `q` 누락 → 400, 정상 응답 JSON 구조 검증, `limit` 상한 초과 → 400.
- `RouteControllerTest`: 미존재 placeId → 404, mode 누락 시 기본값 `SHORTEST` 적용.
- `AdminControllerTest`: Place 생성 후 200/201과 Location 헤더, `entrances` POST 시 PlaceEntrance가 생성되는지.

### 통합 테스트 (Testcontainers)
- `RouteIntegrationTest`: PG+ES 컨테이너 기동 → 시드 → `/api/route?...&mode=INDOOR_PREFERRED` 호출 → polyline·steps 검증. (라우팅 도메인 로직 검증은 [ROUTING.md](ROUTING.md), 본 영역은 HTTP 계약 검증.)
- `SearchIntegrationTest`: `/api/search/autocomplete?q=T` 호출 → 응답 형식·중복 제거 검증. (검색 도메인 로직 검증은 [SEARCH.md](SEARCH.md).)
- `AdminFlowTest`:
- Place 생성 → `/api/places/{id}`로 조회 가능.
- PlaceEntrance 등록 → 곧바로 `/api/route` 호출 시 새 입구가 사용 가능.
- Node/Edge 생성 → `/api/admin/graph/reload` 호출 후 그 노드를 경유하는 경로가 라우팅 결과에 등장.
- `/api/admin/index/reindex` 호출 후 자동완성이 정상 동작.

### 수동 검증 (curl 시나리오)

```bash
docker-compose up -d
./gradlew bootRun

# 자동완성
curl "http://localhost:8080/api/search/autocomplete?q=공학"

# 상세 조회
curl "http://localhost:8080/api/places/1"

# 길찾기 (3가지 모드를 같은 from/to로)
curl "http://localhost:8080/api/route?fromPlaceId=1&toPlaceId=2&mode=SHORTEST"
curl "http://localhost:8080/api/route?fromPlaceId=1&toPlaceId=2&mode=INDOOR_PREFERRED"
curl "http://localhost:8080/api/route?fromPlaceId=1&toPlaceId=2&mode=STAIR_AVOIDANCE"

# 모드별로 summary.distanceMeters와 summary.floorChanges가 달라지는지 확인.
```

## 참조 (References)

- 라우팅 도메인: [ROUTING.md](ROUTING.md)
- 검색 도메인: [SEARCH.md](SEARCH.md)
- 엔티티: [DATA_MODEL.md](DATA_MODEL.md)
- 인프라(애플리케이션 기동): [INFRASTRUCTURE.md](INFRASTRUCTURE.md)
- 설계 배경: [../DRAFT.md](../DRAFT.md) §4.7, §5.6, §5.7
127 changes: 127 additions & 0 deletions .claude/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# ARCHITECTURE.md — 시스템 청사진 (참조 전용)

> 이 파일은 **참조 전용**입니다. 작업 단위(목표·선행조건·변경 파일·구현·검증) 절차는 [PLAN.md](PLAN.md) 인덱스의 각 영역 파일을 참고하세요.

---

## Context

본 시스템은 [DRAFT.md](../DRAFT.md)의 설계 초안을 바탕으로 사용자의 결정사항을 반영한 **Spring Boot 기반 교내 시설 길찾기 백엔드**다. 현재 백엔드는 Spring Boot 4.0.6 / Java 21 환경의 빈 프로젝트(`MapApplication.java`만 존재) 상태이며, DRAFT 대비 다음 핵심 차이를 반영한다.

- **장소 모델 변경**: `Place.entranceNodeId`(1:1) → `PlaceEntrance` 조인 테이블(1:N). 하나의 장소가 정문/후문/연결통로 등 여러 출입 노드를 가질 수 있다.
- **Building 엔티티 통합**: 별도 `Building` 테이블 폐기. `Place.kind = BUILDING | ROOM | FACILITY`로 통합하고 `parent_place_id`로 강의실↔건물 관계 표현. 별칭은 `PlaceAlias` 단일 테이블.
- **검색 모드 추가**: `SHORTEST | INDOOR_PREFERRED | STAIR_AVOIDANCE` 세 모드(단일 선택). ACCESSIBLE(휠체어) 모드는 본 프로젝트에서 다루지 않음.
- **검색엔진**: Elasticsearch 채택. 단, `SearchService` 인터페이스를 두어 구현체 교체 가능 구조.
- **회전 지시 규칙**: 실외 경로에서만 회전 step 생성, 실내 회전은 지시에서 제외.

목적: DRAFT가 정의한 검색·길찾기 도메인을 구현 가능한 코드 단위로 분해하고, 진행 순서·파일 구조·API 계약·검증 방법을 확정한다.

---

## 1. 기술 스택

| 영역 | 선택 | 비고 |
|------|------|------|
| 프레임워크 | Spring Boot 4.0.6 / Java 21 | 현 build.gradle 유지 |
| ORM | Spring Data JPA + Hibernate | `@JdbcTypeCode(SqlTypes.JSON)`으로 JSONB 매핑 |
| DB | PostgreSQL | JSONB 컬럼 사용 |
| 마이그레이션 | Flyway | `src/main/resources/db/migration/` |
| 좌표계 | `double lat/lng` (PostGIS 미사용) | Haversine으로 거리 계산 |
| 검색엔진 | Elasticsearch | `SearchService` 인터페이스 + ES 구현체 |
| 그래프 | 기동 시 인메모리 인접 리스트 적재 | 데이터 변경 시 갱신 |
| 인증 | **없음 (1차 프로토타입)** | 운영 전 반드시 추가 |
| 로컬 환경 | docker-compose (PG + ES) | 프로젝트 루트 `docker-compose.yml` |

세부 의존성 목록은 [INFRASTRUCTURE.md](INFRASTRUCTURE.md)를 참조한다.

---

## 2. 패키지 구조

```
com.honggwart.map
├── MapApplication.java
├── config/
│ ├── ElasticsearchConfig.java
│ └── JpaConfig.java
├── domain/
│ ├── place/ # Place, PlaceAlias, PlaceEntrance, PlaceKind
│ ├── graph/ # Node, Edge, NodeType, EdgeType
│ └── common/ # 공통 enum, 값 객체
├── search/
│ ├── api/ # SearchController, DTO
│ ├── service/ # SearchService(인터페이스), ESSearchService
│ ├── indexer/ # PlaceIndexer (트랜잭션 이벤트 리스너)
│ └── es/ # ES 도큐먼트, repository
├── routing/
│ ├── api/ # RouteController, DTO
│ ├── service/ # RouteService, RoutingMode
│ ├── graph/ # GraphRegistry(인메모리 그래프), GraphLoader
│ ├── algorithm/ # Dijkstra, WeightPolicy
│ └── instruction/ # StepExtractor, BearingCalculator
└── admin/
└── api/ # AdminController (CRUD)
```
Comment on lines +41 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

다이어그램 코드 블록에 언어를 지정해 주세요.

Line 41, Line 70의 fenced code block에 언어 지정이 없어 markdownlint(MD040) 경고가 납니다. 예를 들어 패키지 트리는 text, 아키텍처 다이어그램은 text 또는 mermaid로 명시해 주세요.

Also applies to: 70-108

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 41-41: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/ARCHITECTURE.md around lines 41 - 64, Add explicit fenced-code
languages to the two code blocks to satisfy MD040: for the package tree block
that starts with "com.honggwart.map" (which lists MapApplication.java, config/,
domain/, search/, routing/, admin/) prefix the opening ``` with a language like
text (```text), and for the architecture/diagram block later (the other fenced
block covering the architecture diagram) choose an appropriate language tag such
as text or mermaid (e.g., ```mermaid if it is a diagram). Ensure both fenced
blocks include the language identifier and re-run linting.


---

## 3. 영역 경계 다이어그램

```
┌──────────────────────┐
│ API.md (Controller) │
│ ───────────────── │
│ SearchController │
│ RouteController │
│ AdminController │
└─────┬───────┬───────┬─┘
│ │ │
┌───────▼─┐ ┌─▼──────▼──────┐
│SEARCH.md│ │ ROUTING.md │
│ ────── │ │ ────────── │
│ Service │ │ Service │
│ Indexer │ │ GraphRegistry │
│ ES Doc │ │ Dijkstra │
└──┬──────┘ │ WeightPolicy │
│ │ StepExtractor │
│ └─────┬─────────┘
│ │
│ ┌────────▼────────────┐
│ │ DATA_MODEL.md │
└──────▶│ ─────────────────── │
│ Place / Alias / │
│ Entrance / Node / │
│ Edge (JPA + DDL) │
└─────────┬───────────┘
┌─────────▼───────────┐
│ INFRASTRUCTURE.md │
│ ─────────────────── │
│ build.gradle │
│ application.yml │
│ docker-compose │
│ Flyway V1 │
└─────────────────────┘

외부: PostgreSQL ◀───┘
Elasticsearch ◀── (SEARCH.md indexer + service)
```

핵심 연결 지점:
- **검색 ↔ 그래프**: `Place ─< PlaceEntrance >─ Node`. 사용자는 Place를 검색·선택하고, 라우팅 서비스가 PlaceEntrance의 다중 node_id 집합을 가상 노드로 묶어 Dijkstra를 수행한다.
- **검색 색인 동기화**: Place 트랜잭션 커밋 후 `PlacePersistedEvent` → `PlaceIndexer` → ES upsert (동기).
- **그래프 동기화**: Node/Edge 트랜잭션 커밋 후 `GraphRegistry.reload()` 호출.

---

## 4. 영역 파일 매핑

| 영역 | 책임 | 파일 |
|------|------|------|
| 인프라 부트스트랩 | 의존성·환경·DDL 초기화 | [INFRASTRUCTURE.md](INFRASTRUCTURE.md) |
| 데이터 모델 | DDL·JPA 엔티티·시드 | [DATA_MODEL.md](DATA_MODEL.md) |
| 라우팅 | 그래프·Dijkstra·step 추출 | [ROUTING.md](ROUTING.md) |
| 검색 | ES 색인·서비스·동기화 | [SEARCH.md](SEARCH.md) |
| REST API | 검색·라우팅·관리자 컨트롤러 | [API.md](API.md) |

원본 설계 초안은 [DRAFT.md](../DRAFT.md)에 있다.
Loading