Skip to content

Commit 65543c6

Browse files
authored
Merge pull request #99 from SEMOSAN/feat/#49-live-activity
Feat/#49 live activity
2 parents 366a9e2 + 6785eac commit 65543c6

12 files changed

Lines changed: 316 additions & 36 deletions

File tree

CLAUDE.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Communication
6+
7+
- 한국어로 답변한다.
8+
- "확인해줘", "봐줘", "뭐가 문제야" 같은 표현은 분석만 요청하는 것으로 간주하고 코드를 수정하지 않는다.
9+
- "수정해줘", "고쳐줘", "반영해줘", "만들어줘", "추가해줘", "삭제해줘" 같은 명시적 요청이 있을 때만 파일을 편집한다.
10+
- 수정 전에 어떤 파일을 왜 건드릴지 먼저 설명하고, 명시적 승인을 기다린다.
11+
12+
## Build & Test Commands
13+
14+
```bash
15+
./gradlew build # 전체 빌드
16+
./gradlew test # 전체 테스트
17+
./gradlew test --tests <fully.qualified.TestClass> # 단일 클래스 테스트
18+
./gradlew bootRun # 애플리케이션 실행 (local 프로파일)
19+
```
20+
21+
로컬 인프라(PostgreSQL, Redis, MinIO)가 없으면 테스트가 실패할 수 있다. 인프라 부재로 인한 실패와 코드 문제로 인한 실패를 명확히 구분해서 보고한다.
22+
23+
## Architecture Overview
24+
25+
### Package Structure
26+
27+
```text
28+
com.semosan.api
29+
├── common/ # 공통 인프라 (응답, 예외, JWT, 설정, FCM, 알림 공통)
30+
└── domain/ # 도메인별 비즈니스 로직
31+
├── auth/ # JWT 로그인/로그아웃/토큰 재발급, 회원탈퇴
32+
├── oauth/ # 카카오·애플 소셜 로그인
33+
├── user/ # 사용자 프로필, 온보딩, 알림 설정
34+
├── mountain/ # 산 정보, 코스, 좋아요, 지도 검색 (PostGIS)
35+
├── hiking/ # 등산 기록 조회
36+
├── tracking/ # 실시간 GPS 트래킹 (WebSocket STOMP + Redis Stream)
37+
├── community/ # 자유게시글·기록게시글, 댓글, 좋아요
38+
├── image/ # MinIO presigned URL 발급
39+
├── notification/# FCM 토큰, 알림 엔티티, 이벤트
40+
└── review/ # 산 리뷰
41+
```
42+
43+
### Layer Convention
44+
45+
각 도메인은 `controller → service → repository` 계층을 따른다.
46+
Swagger 문서는 컨트롤러와 분리된 `controller/docs/<Name>ControllerDocs` 인터페이스에 작성하고, 컨트롤러가 이 인터페이스를 구현한다.
47+
48+
DTO는 다음 규칙을 따른다.
49+
- 요청: `dto/request/` 하위 또는 `dto/` 직하위에 `*Request.java`
50+
- 응답: `dto/response/` 하위 또는 `dto/` 직하위에 `*Response.java`
51+
- 서비스 간 전달 커맨드: `dto/command/` 하위에 `*Command.java`
52+
53+
### API Response Convention
54+
55+
모든 컨트롤러는 `ApiResponse<T>`로 응답을 반환한다.
56+
57+
```java
58+
// 데이터 없는 성공
59+
return ApiResponse.success(SuccessStatus.XXX);
60+
61+
// 데이터 포함 성공
62+
return ApiResponse.success(SuccessStatus.XXX, data);
63+
64+
// 오류
65+
throw new GeneralException(ErrorStatus.XXX);
66+
```
67+
68+
`SuccessStatus``ErrorStatus` 모두 `BaseStatus` 인터페이스를 구현하는 enum이다.
69+
새 에러 코드는 `ErrorStatus`에, 성공 코드는 `SuccessStatus`에 추가한다.
70+
71+
### Authentication
72+
73+
- Stateless JWT 방식. `JwtFilter``Authorization: Bearer <token>` 헤더를 검증한다.
74+
- WebSocket(STOMP) 연결은 HTTP 필터를 통과(`/ws/tracking/**` permitAll)하고, `StompAuthChannelInterceptor`가 STOMP CONNECT 프레임에서 JWT를 검증한다.
75+
- 퍼블릭 엔드포인트: 스웨거, OAuth 로그인, 토큰 재발급, `/api/auth/test/login`
76+
77+
### Real-time GPS Tracking Flow
78+
79+
1. 클라이언트가 WebSocket(STOMP)으로 GPS 좌표를 전송
80+
2. `TrackingGpsPublisher`가 Redis Stream(`tracking:gps`)에 publish
81+
3. `TrackingStreamConsumer`가 스트림을 consume → 실시간 통계(Redis Hash) 갱신 + 메모리 버퍼 적재
82+
4. 버퍼가 100개 이상이거나 10초 주기 스케줄러가 동작하면 `TrackingPointFlushService`가 DB에 배치 insert
83+
5. 세션 종료 시 `TrackingSessionTerminatedEvent`로 잔여 버퍼를 final flush
84+
85+
`TrackingPointFlushService`는 self-invocation AOP 우회를 위해 별도 빈으로 분리되어 있다.
86+
87+
### Database & Migrations
88+
89+
- PostgreSQL + PostGIS (지리 좌표 검색에 `hibernate-spatial` + JTS 사용)
90+
- **prod 프로파일**: Flyway 활성화, `ddl-auto: validate`
91+
- **local 프로파일**: Flyway 비활성화, `ddl-auto: update`
92+
- 마이그레이션 스크립트: `src/main/resources/db/migration/V{N}__description.sql`
93+
94+
### Infrastructure
95+
96+
| 컴포넌트 | 용도 |
97+
|---------|------|
98+
| PostgreSQL | 메인 DB |
99+
| Redis | JWT refresh token 블랙리스트, GPS 실시간 통계 (Hash), GPS 이벤트 스트림 (Stream) |
100+
| MinIO | 이미지 오브젝트 스토리지 (presigned URL 방식) |
101+
| Firebase FCM | 푸시 알림 |
102+
103+
### Environment Variables
104+
105+
`application.yaml`은 환경변수만 참조한다. 로컬 개발은 `application-local.yaml`에 기본값이 하드코딩되어 있다(로컬 전용).
106+
prod에서 필요한 주요 변수: `DB_URL`, `DB_USERNAME`, `DB_PASSWORD`, `REDIS_HOST`, `REDIS_PORT`, `JWT_SECRET`, `JWT_ACCESS_TOKEN_EXPIRATION`, `JWT_REFRESH_TOKEN_EXPIRATION`, `KAKAO_CLIENT_ID`, `KAKAO_CLIENT_SECRET`, `KAKAO_REDIRECT_URI`, `KAKAO_ADMIN_KEY`, `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_PUBLIC_URL`, `TRACKING_STREAM_KEY`, `TRACKING_CONSUMER_GROUP`, `FIREBASE_SERVICE_ACCOUNT_PATH`, `DISCORD_ALERT_ENABLED`, `DISCORD_WEBHOOK_URL`
107+
108+
## Branch & Commit Convention
109+
110+
브랜치명:
111+
```text
112+
feat/#이슈번호-기능명
113+
fix/#이슈번호-버그명
114+
```
115+
116+
커밋 타입: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
117+
118+
## PR Rules
119+
120+
- 리뷰어 2명 전원 승인 후 머지 (셀프 머지 금지)
121+
122+
## Swagger
123+
124+
로컬 실행 후 `http://localhost:8080/swagger-ui.html` 에서 API 문서 확인 가능.

src/main/java/com/semosan/api/common/status/ErrorStatus.java

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,22 @@ public enum ErrorStatus implements BaseStatus {
4949
/**
5050
* User
5151
*/
52-
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_1", "사용자를 찾을 수 없습니다."),
53-
ONBOARDING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "USER_409_1", "이미 온보딩을 완료한 사용자입니다."),
5452
PREFERRED_DIFFICULTY_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_1", "숙련자는 선호 난이도를 선택해야 합니다."),
5553
PREFERRED_DIFFICULTY_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_2", "선호 난이도는 숙련자만 선택할 수 있습니다."),
5654
PROFILE_UPDATE_FIELD_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_3", "수정할 프로필 정보가 없습니다."),
57-
NOTIFICATION_SETTING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_2", "알림 설정을 찾을 수 없습니다."),
5855
INVALID_NICKNAME(HttpStatus.BAD_REQUEST, "USER_400_4", "사용할 수 없는 닉네임입니다."),
59-
DUPLICATED_NICKNAME(HttpStatus.CONFLICT, "USER_409_2", "이미 사용 중인 닉네임입니다."),
6056
UNDER_AGE_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_5", "만 14세 미만은 가입할 수 없습니다."),
6157
INVALID_NICKNAME_FORMAT(HttpStatus.BAD_REQUEST, "USER_400_6", "닉네임 형식이 올바르지 않습니다."),
6258
NICKNAME_RESERVED(HttpStatus.BAD_REQUEST, "USER_400_7", "사용할 수 없는 사칭 표현이 포함된 닉네임입니다."),
6359
NICKNAME_BLOCKED_WORD(HttpStatus.BAD_REQUEST, "USER_400_8", "금칙어가 포함된 닉네임입니다."),
6460
EXERCISE_DETAIL_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_9", "운동 빈도와 운동 시간을 입력해야 합니다."),
6561
EXERCISE_DETAIL_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "USER_400_10", "운동 안함 선택 시 운동 빈도와 운동 시간을 입력할 수 없습니다."),
66-
ONBOARDING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_3", "온보딩 정보를 찾을 수 없습니다."),
6762
ONBOARDING_NOT_COMPLETED(HttpStatus.FORBIDDEN, "USER_403_1", "온보딩을 완료해야 이용할 수 있습니다."),
63+
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_1", "사용자를 찾을 수 없습니다."),
64+
NOTIFICATION_SETTING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_2", "알림 설정을 찾을 수 없습니다."),
65+
ONBOARDING_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_3", "온보딩 정보를 찾을 수 없습니다."),
66+
ONBOARDING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "USER_409_1", "이미 온보딩을 완료한 사용자입니다."),
67+
DUPLICATED_NICKNAME(HttpStatus.CONFLICT, "USER_409_2", "이미 사용 중인 닉네임입니다."),
6868

6969
/**
7070
* Notification
@@ -74,17 +74,18 @@ public enum ErrorStatus implements BaseStatus {
7474
/**
7575
* Apple OAuth
7676
*/
77-
APPLE_PUBLIC_KEY_REQUEST_FAILED(HttpStatus.BAD_GATEWAY, "APPLE_502_1", "애플 공개키 조회에 실패했습니다."),
7877
APPLE_PUBLIC_KEY_NOT_FOUND(HttpStatus.UNAUTHORIZED, "APPLE_401_1", "유효한 애플 공개키를 찾을 수 없습니다."),
7978
APPLE_IDENTITY_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "APPLE_401_2", "애플 identity token이 유효하지 않습니다."),
79+
APPLE_PUBLIC_KEY_REQUEST_FAILED(HttpStatus.BAD_GATEWAY, "APPLE_502_1", "애플 공개키 조회에 실패했습니다."),
8080

8181
/**
8282
* Mountain
8383
*/
8484
MOUNTAIN_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_1", "산을 찾을 수 없습니다."),
8585
MOUNTAIN_LIKE_ALREADY_EXISTS(HttpStatus.CONFLICT, "MTN_409_1", "이미 좋아요한 산입니다."),
86-
MOUNTAIN_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_2", "좋아요한 산이 아닙니다."),
8786
MOUNTAIN_BBOX_PARTIAL(HttpStatus.BAD_REQUEST, "MTN_400_1", "BBox 좌표는 4개(swLat, swLng, neLat, neLng) 모두 보내거나 모두 비워주세요."),
87+
MOUNTAIN_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_2", "좋아요한 산이 아닙니다."),
88+
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),
8889

8990
/**
9091
* Image
@@ -102,45 +103,45 @@ public enum ErrorStatus implements BaseStatus {
102103
/**
103104
* Hiking (등산 기록)
104105
*/
105-
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
106106
HIKING_RECORD_FORBIDDEN(HttpStatus.FORBIDDEN, "HIKE_403_1", "본인이 참여한 등산 기록만 공유할 수 있습니다."),
107+
HIKING_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "HIKE_404_1", "등산 기록을 찾을 수 없습니다."),
107108

108109
/**
109110
* Tracking (트래킹 세션)
110111
*/
111-
TRACKING_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_1", "트래킹 세션을 찾을 수 없습니다."),
112+
TRACKING_COURSE_MOUNTAIN_MISMATCH(HttpStatus.BAD_REQUEST, "TRK_400_1", "선택한 코스가 해당 산의 코스가 아닙니다."),
113+
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
112114
TRACKING_SESSION_FORBIDDEN(HttpStatus.FORBIDDEN, "TRK_403_1", "본인의 트래킹 세션만 조작할 수 있습니다."),
115+
TRACKING_SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_1", "트래킹 세션을 찾을 수 없습니다."),
116+
TRACKING_PHOTO_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_2", "트래킹 사진을 찾을 수 없습니다."),
113117
TRACKING_SESSION_ALREADY_IN_PROGRESS(HttpStatus.CONFLICT, "TRK_409_1", "이미 진행 중인 트래킹 세션이 있습니다."),
114118
TRACKING_SESSION_INVALID_STATE(HttpStatus.CONFLICT, "TRK_409_2", "현재 상태에서는 수행할 수 없는 작업입니다."),
115-
TRACKING_COURSE_MOUNTAIN_MISMATCH(HttpStatus.BAD_REQUEST, "TRK_400_1", "선택한 코스가 해당 산의 코스가 아닙니다."),
116-
TRACKING_COURSE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "TRK_400_2", "자유 기록이 아니면 코스 ID는 필수입니다."),
117119
TRACKING_PHOTO_DUPLICATE(HttpStatus.CONFLICT, "TRK_409_3", "해당 마일스톤에 이미 업로드된 사진이 있습니다."),
118120
TRACKING_PHOTO_SESSION_INACTIVE(HttpStatus.CONFLICT, "TRK_409_4", "활성 상태가 아닌 세션에는 사진을 업로드할 수 없습니다."),
119-
TRACKING_PHOTO_NOT_FOUND(HttpStatus.NOT_FOUND, "TRK_404_2", "트래킹 사진을 찾을 수 없습니다."),
120-
COURSE_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_3", "코스를 찾을 수 없습니다."),
121+
TRACKING_COURSE_POLYLINE_REQUIRED(HttpStatus.UNPROCESSABLE_ENTITY, "TRK_422_1", "코스 경로 좌표가 등록되지 않았습니다."),
121122

122123
/**
123124
* Post (게시글 공통)
124125
*/
125-
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_404_1", "게시글을 찾을 수 없습니다."),
126-
POST_DELETED(HttpStatus.NOT_FOUND, "POST_404_2", "삭제된 게시글입니다."),
127-
POST_FORBIDDEN(HttpStatus.FORBIDDEN, "POST_403_1", "본인의 게시글만 처리할 수 있습니다."),
128126
POST_CONTENT_REQUIRED(HttpStatus.BAD_REQUEST, "POST_400_1", "본문은 비어있을 수 없습니다."),
129127
POST_IMAGE_INDEX_INVALID(HttpStatus.BAD_REQUEST, "POST_400_2", "대표 이미지 인덱스가 잘못되었습니다."),
128+
POST_FORBIDDEN(HttpStatus.FORBIDDEN, "POST_403_1", "본인의 게시글만 처리할 수 있습니다."),
129+
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_404_1", "게시글을 찾을 수 없습니다."),
130+
POST_DELETED(HttpStatus.NOT_FOUND, "POST_404_2", "삭제된 게시글입니다."),
130131

131132
/**
132133
* Comment (댓글/대댓글)
133134
*/
135+
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다."),
136+
COMMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "CMT_403_1", "본인의 댓글만 처리할 수 있습니다."),
134137
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "CMT_404_1", "댓글을 찾을 수 없습니다."),
135138
COMMENT_DELETED(HttpStatus.NOT_FOUND, "CMT_404_2", "삭제된 댓글입니다."),
136-
COMMENT_FORBIDDEN(HttpStatus.FORBIDDEN, "CMT_403_1", "본인의 댓글만 처리할 수 있습니다."),
137-
COMMENT_PARENT_POST_MISMATCH(HttpStatus.BAD_REQUEST, "CMT_400_1", "부모 댓글이 같은 게시글의 댓글이 아닙니다."),
138139

139140
/**
140141
* SemoFeed (세모피드)
141142
*/
142-
SEMOFEED_NOT_FOUND(HttpStatus.NOT_FOUND, "SF_404_1", "세모피드를 찾을 수 없습니다."),
143-
SEMOFEED_FORBIDDEN(HttpStatus.FORBIDDEN, "SF_403_1", "본인의 세모피드만 처리할 수 있습니다.");
143+
SEMOFEED_FORBIDDEN(HttpStatus.FORBIDDEN, "SF_403_1", "본인의 세모피드만 처리할 수 있습니다."),
144+
SEMOFEED_NOT_FOUND(HttpStatus.NOT_FOUND, "SF_404_1", "세모피드를 찾을 수 없습니다.");
144145

145146
private final HttpStatus httpStatus;
146147
private final String code;

src/main/java/com/semosan/api/common/status/SuccessStatus.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,25 @@ public enum SuccessStatus implements BaseStatus {
5656
* Tracking — 진입 화면 (#45) & 세션 (#18)
5757
*/
5858
TRACKING_NEAREST_MOUNTAIN_SUCCESS(HttpStatus.OK, "TRK_200_1", "현재 위치 기준 가까운 산과 코스 조회에 성공했습니다."),
59-
TRACKING_SESSION_CREATE_SUCCESS(HttpStatus.CREATED, "TRK_201_1", "트래킹 세션이 시작되었습니다."),
6059
TRACKING_SESSION_GET_ACTIVE_SUCCESS(HttpStatus.OK, "TRK_200_2", "현재 진행 중인 트래킹 세션 조회에 성공했습니다."),
6160
TRACKING_SESSION_GET_SUCCESS(HttpStatus.OK, "TRK_200_3", "트래킹 세션 상세 조회에 성공했습니다."),
6261
TRACKING_SESSION_PAUSE_SUCCESS(HttpStatus.OK, "TRK_200_4", "트래킹 세션을 일시정지했습니다."),
6362
TRACKING_SESSION_RESUME_SUCCESS(HttpStatus.OK, "TRK_200_5", "트래킹 세션을 재개했습니다."),
6463
TRACKING_SESSION_COMPLETE_SUCCESS(HttpStatus.OK, "TRK_200_6", "트래킹 세션을 종료했습니다."),
6564
TRACKING_SESSION_ABANDON_SUCCESS(HttpStatus.OK, "TRK_200_7", "트래킹 세션을 포기 처리했습니다."),
65+
TRACKING_LIVE_ACTIVITY_COURSE_SUCCESS(HttpStatus.OK, "TRK_200_8", "라이브 액티비티용 코스 정보 조회에 성공했습니다."),
66+
TRACKING_PHOTO_LIST_SUCCESS(HttpStatus.OK, "TRK_200_9", "트래킹 사진 목록 조회에 성공했습니다."),
67+
TRACKING_SESSION_CREATE_SUCCESS(HttpStatus.CREATED, "TRK_201_1", "트래킹 세션이 시작되었습니다."),
6668
TRACKING_PHOTO_UPLOAD_SUCCESS(HttpStatus.CREATED, "TRK_201_2", "트래킹 사진이 저장되었습니다."),
67-
TRACKING_PHOTO_LIST_SUCCESS(HttpStatus.OK, "TRK_200_8", "트래킹 사진 목록 조회에 성공했습니다."),
6869

6970
/**
7071
* SemoFeed
7172
*/
72-
SEMOFEED_CREATE_SUCCESS(HttpStatus.CREATED, "SF_201_1", "세모피드가 저장되었습니다."),
7373
SEMOFEED_LIST_SUCCESS(HttpStatus.OK, "SF_200_1", "세모피드 목록 조회에 성공했습니다."),
7474
SEMOFEED_MY_LIST_SUCCESS(HttpStatus.OK, "SF_200_2", "내 세모피드 목록 조회에 성공했습니다."),
7575
SEMOFEED_TOGGLE_PUBLIC_SUCCESS(HttpStatus.OK, "SF_200_3", "세모피드 공개 상태가 변경되었습니다."),
7676
SEMOFEED_DELETE_SUCCESS(HttpStatus.OK, "SF_200_4", "세모피드가 삭제되었습니다."),
77+
SEMOFEED_CREATE_SUCCESS(HttpStatus.CREATED, "SF_201_1", "세모피드가 저장되었습니다."),
7778

7879
/**
7980
* Image
@@ -99,11 +100,11 @@ public enum SuccessStatus implements BaseStatus {
99100
/**
100101
* Comment (댓글/대댓글)
101102
*/
102-
COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "CMT_201_1", "댓글이 작성되었습니다."),
103-
COMMENT_REPLY_SUCCESS(HttpStatus.CREATED, "CMT_201_2", "대댓글이 작성되었습니다."),
104103
COMMENT_LIST_SUCCESS(HttpStatus.OK, "CMT_200_1", "댓글 목록 조회에 성공했습니다."),
105104
COMMENT_REPLY_LIST_SUCCESS(HttpStatus.OK, "CMT_200_2", "대댓글 목록 조회에 성공했습니다."),
106105
COMMENT_DELETE_SUCCESS(HttpStatus.OK, "CMT_200_3", "댓글이 삭제되었습니다."),
106+
COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "CMT_201_1", "댓글이 작성되었습니다."),
107+
COMMENT_REPLY_SUCCESS(HttpStatus.CREATED, "CMT_201_2", "대댓글이 작성되었습니다."),
107108

108109
/**
109110
* Post Like (좋아요)
@@ -114,12 +115,12 @@ public enum SuccessStatus implements BaseStatus {
114115
/**
115116
* Free Post (자유게시판 게시글)
116117
*/
117-
FREE_POST_CREATE_SUCCESS(HttpStatus.CREATED, "FPOST_201_1", "자유게시판 게시글이 작성되었습니다."),
118118
FREE_POST_LIST_SUCCESS(HttpStatus.OK, "FPOST_200_1", "자유게시판 게시글 목록 조회에 성공했습니다."),
119119
FREE_POST_MY_LIST_SUCCESS(HttpStatus.OK, "FPOST_200_2", "내 자유게시판 게시글 목록 조회에 성공했습니다."),
120120
FREE_POST_DETAIL_SUCCESS(HttpStatus.OK, "FPOST_200_3", "자유게시판 게시글 상세 조회에 성공했습니다."),
121121
FREE_POST_DELETE_SUCCESS(HttpStatus.OK, "FPOST_200_4", "자유게시판 게시글이 삭제되었습니다."),
122-
FREE_POST_SEARCH_SUCCESS(HttpStatus.OK, "FPOST_200_5", "자유게시판 게시글 검색에 성공했습니다.");
122+
FREE_POST_SEARCH_SUCCESS(HttpStatus.OK, "FPOST_200_5", "자유게시판 게시글 검색에 성공했습니다."),
123+
FREE_POST_CREATE_SUCCESS(HttpStatus.CREATED, "FPOST_201_1", "자유게시판 게시글이 작성되었습니다.");
123124

124125
private final HttpStatus httpStatus;
125126
private final String code;

src/main/java/com/semosan/api/domain/mountain/entity/Course.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.semosan.api.domain.mountain.enums.Difficulty;
55
import jakarta.persistence.*;
66
import lombok.*;
7+
import org.locationtech.jts.geom.LineString;
78

89
@Table(name = "courses")
910
@Getter
@@ -33,4 +34,7 @@ public class Course extends BaseEntity {
3334

3435
@Column(name = "duration", nullable = false)
3536
private Integer duration;
37+
38+
@Column(name = "polyline", columnDefinition = "geography(LineString, 4326)")
39+
private LineString polyline;
3640
}

0 commit comments

Comments
 (0)