Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
596ec60
feat: 회원 정보 조회 기능 구현
Kyoung-M1N Aug 9, 2026
501ee6e
feat: 닉네임 변경 기능 구현
Kyoung-M1N Aug 10, 2026
128a65c
feat: 프로필 이미지 url 발급 및 변경 기능 구현
Kyoung-M1N Aug 14, 2026
12a2d45
refactor: 파일 확장자 추출 로직 중복 제거
Kyoung-M1N Aug 15, 2026
4f1e71f
feat: 타임라인 삭제 API 구현
youtheyeon Aug 15, 2026
6b8b56d
refactor: 엔드포인트 이름 수정
Kyoung-M1N Aug 16, 2026
d3a073f
feat: 타임라인 삭제 시 S3 이미지 비동기 삭제 구현
youtheyeon Aug 16, 2026
692350a
feat: 타임라인 제목 수정 API 구현
youtheyeon Aug 16, 2026
a3a66b0
fix: 타임라인 삭제 이벤트 리스너 @EnableAsync 누락으로 동기 실행되던 문제 수정
youtheyeon Aug 16, 2026
bb6e0d9
refactor: 타임라인 소유 여부와 조회 쿼리를 단일 쿼리로 통합
youtheyeon Aug 16, 2026
b214826
feat: 타임라인 생성 응답에 timelineId 추가
youtheyeon Aug 16, 2026
754876d
feat: 타임라인 제목 수정 응답에 timelineId, title 추가
youtheyeon Aug 16, 2026
d2f7660
fix: S3 객체 일부 삭제 실패 시 예외로 감지하도록 수정
youtheyeon Aug 17, 2026
97d51f3
refactor: 프로필 이미지 경로에 member prefix 적용
Kyoung-M1N Aug 18, 2026
088be30
fix: 파일 확장자 추출에서의 예외처리 추가
Kyoung-M1N Aug 19, 2026
a201341
fix: 타임라인 이미지 삭제 시 전역 @EnableAsync 대신 가상 스레드로 S3 I/O 분리
youtheyeon Aug 19, 2026
5297017
[FEAT/#88] 회원 정보 수정 기능 구현 (#94)
Kyoung-M1N Aug 21, 2026
25796a8
refactor: 타임라인 삭제 트랜잭션을 TransactionTemplate으로 분리
youtheyeon Aug 21, 2026
98db935
chore: pull develop
youtheyeon Aug 21, 2026
6946b09
fix: S3 삭제 어댑터가 assembleUploadObjectKey 대신 assemble 사용하도록 수정
youtheyeon Aug 21, 2026
b8f764b
[FEAT/#91] 타임라인 수정 및 삭제 API 구현 (#92)
youtheyeon Aug 22, 2026
09c47ad
feat: 활동 생성 시 비정상 종료 활동 삭제 기능 추가
khj011219 Aug 22, 2026
69e1a02
deploy: 운영/테스트 서버 분리에 따른 배포 워크플로우 분리
youtheyeon Aug 22, 2026
4ce615e
feat: 비정상 활동 timeline 이미지 삭제 기능 추가
khj011219 Aug 22, 2026
90f3479
test: 테스트 서버 배포 워크플로우 검증
youtheyeon Aug 22, 2026
b183fa5
chore: 테스트용 배포 브랜치 트리거 제거
youtheyeon Aug 22, 2026
dd15582
fix: ActivityImageUrlServiceTest 오류 수정
khj011219 Aug 22, 2026
8ef384a
test: ActivityServiceTest 코드 수정
khj011219 Aug 22, 2026
e622576
refactor: 타임라인 이미지 삭제 대상을 인터페이스 프로젝션으로 전환
khj011219 Aug 23, 2026
2ce4b5f
refactor: 활동 생성을 직렬화하여 중복 활동 생성 방지
khj011219 Aug 23, 2026
0f73e42
refactor: 미사용 예외 처리 삭제
khj011219 Aug 23, 2026
9c42812
[FEAT/#95] 활동 시작 시 비정상 활동 종료 기능 구현 (#98)
khj011219 Aug 24, 2026
0992ea5
fix: memberId 기반 prefix 추가 반영
Kyoung-M1N Aug 25, 2026
a54524f
refactor: 기존 삭제 로직에 변경된 키관리 구조 적용
Kyoung-M1N Aug 25, 2026
81ea16b
fix: 프로필 이미지 object key 기반으로 변경
Kyoung-M1N Aug 25, 2026
c586600
feat: 회원탈퇴, 프로필 이미지 변경 시 S3 파일 삭제 추가
Kyoung-M1N Aug 25, 2026
84bb8e9
fix: 지도 이미지 object key 방식으로 변경
Kyoung-M1N Aug 25, 2026
e4ae741
fix: 타임라인 삭제 시 summary의 커버 이미지 변경 및 object key 기반으로 변경
Kyoung-M1N Aug 25, 2026
f19b63c
fix: 커버 이미지와 activityId 그룹화 적용
Kyoung-M1N Aug 25, 2026
7617cdb
fix: 프로필 이미지 동등 비교 조건 추가
Kyoung-M1N Aug 25, 2026
8d6120f
fix: 완료 이벤트 삭제 설정 추가
Kyoung-M1N Aug 26, 2026
81a6bc1
fix: 삭제시 커버 이미지 기준 탐색 적용
Kyoung-M1N Aug 26, 2026
2a7806f
[FIX/#99] S3 키관리 구조 변경 및 삭제 로직 적용 (#100)
Kyoung-M1N Aug 26, 2026
62c63e2
[FEAT/#96] 운영 및 테스트 서버 배포 워크플로우 분리 (#97)
youtheyeon Aug 26, 2026
b35db9b
[DEPLOY] 테스트 서버 워크플로우 재시작
Kyoung-M1N Aug 27, 2026
189dfd7
hotfix: 비정상 종료 활동 삭제시 외래키 제약조건 충돌 해결
Kyoung-M1N Aug 27, 2026
0d1a494
hotfix: 헬스체크 로직 수정 및 컨테이너 롤백 적용
Kyoung-M1N Aug 27, 2026
fc9d69e
hotfix: 헬스체크 재시도 루프 횟수 증가
Kyoung-M1N Aug 27, 2026
7432298
feat: 약관 동의 여부 관리 엔티티 생성
Kyoung-M1N Aug 29, 2026
cc9949b
hotfix: 기본 프로필 이미지 설정 기능 추가
Kyoung-M1N Aug 30, 2026
f65d2f4
hotfix: 닉네임 규칙 변경사항 적용
Kyoung-M1N Aug 30, 2026
b3f40fe
feat: 회원가입 시 약관 동의 여부 포함
Kyoung-M1N Aug 30, 2026
bbd7f01
Merge branch 'develop' into feat/102/terms-agreement
Kyoung-M1N Aug 31, 2026
2585218
fix: 누락된 필드 추가
Kyoung-M1N Aug 31, 2026
6f13cca
refactor: 항목별 시간대 별도 저장 컬럼 추가
Kyoung-M1N Aug 31, 2026
e064b27
[FEAT/#102] 약관 동의 여부 관련 기능 구현 (#103)
Kyoung-M1N Aug 31, 2026
2706fcc
hotfix: apple bundle id 분리 적용
Kyoung-M1N Sep 1, 2026
a966c19
hotfix: 활동 종료 중복 요청 방어 로직 추가
Kyoung-M1N Sep 3, 2026
8acf51d
fix: 루트 등록 제약 조건 조정
youtheyeon Sep 3, 2026
172fcc5
fix: 루트 목록 빈 배열 요청 방지 롤백
youtheyeon Sep 3, 2026
f63368f
[FIX/#106] 루트 등록 제약 조건 조정 (#107)
youtheyeon Sep 3, 2026
21f3c66
fix: 특정 날짜 활동 목록 조회 최신순 정렬
youtheyeon Sep 4, 2026
22b960c
[FIX/#108] 특정 날짜 활동 목록 조회 최신순 정렬 (#109)
youtheyeon Sep 4, 2026
d23111c
feat: 구글 로그인 구현
khj011219 Sep 6, 2026
a3ddfb5
fix: 리스너 타입 수정
Kyoung-M1N Sep 6, 2026
a0d385e
fix: 중복 요청 시 낙관적 락 예외 처리 적용
Kyoung-M1N Sep 6, 2026
9ff1f4e
[FEAT/#105] 구글 로그인 구현 (#111)
khj011219 Sep 7, 2026
c4f46d4
test: 회원 탈퇴 동시 요청 테스트 작성
Kyoung-M1N Sep 8, 2026
98d4867
[FIX/#110] 회원탈퇴 중복 요청 예외 처리 적용 (#112)
Kyoung-M1N Sep 10, 2026
71d7b8f
feat: revoke 인터페이스 호출 추가
Kyoung-M1N Sep 15, 2026
b16b6e7
chore: 애플 토큰 발급용 라이브러리 의존성 추가
Kyoung-M1N Sep 17, 2026
0d66a33
refactor: revoke 로직 파라미터 및 분기 처리 변경
Kyoung-M1N Sep 17, 2026
b15aebd
feat: revoke용 파라미터 추가
Kyoung-M1N Sep 17, 2026
45e4bb3
chore: revoke 환경변수 추가
Kyoung-M1N Sep 17, 2026
3d99bca
feat: revoke 로직 구현
Kyoung-M1N Sep 17, 2026
93d8adf
test: 기존 서비스 로직 테스트 수정
Kyoung-M1N Sep 17, 2026
9d81c78
fix: 미구현 영역 설명 제거
Kyoung-M1N Sep 17, 2026
17a2ce6
refactor: 외부 요청 timeout 설정
Kyoung-M1N Sep 17, 2026
f08c8b5
style: 불필요 주석 제거
Kyoung-M1N Sep 18, 2026
2d0b331
style: 스웨거 설명 수정
Kyoung-M1N Sep 18, 2026
34f5c24
[FEAT/#113] 회원탈퇴 시 OAuth revoke 연결 (#114)
Kyoung-M1N Sep 18, 2026
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ JWT_SECRET=
# OIDC
APPLE_CLIENT_ID=com.Routee-iOS

# OAuth revoke
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY=
GOOGLE_CLIENT_ID=

# Redis
REDIS_HOST=
REDIS_PORT=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
name: Deploy

on:
push:
branches:
- main

concurrency:
group: deploy
cancel-in-progress: false
workflow_call:
inputs:
environment:
required: true
type: string
secret_prefix:
required: false
type: string
default: ""

jobs:
deploy:
runs-on: ubuntu-latest

concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false

steps:
- uses: actions/checkout@v4

Expand All @@ -33,15 +39,15 @@ jobs:

- name: Build and Push Docker Image
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ github.sha }} \
-t ${{ secrets.DOCKER_USERNAME }}/routee-api:latest .
docker push ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ github.sha }}
docker push ${{ secrets.DOCKER_USERNAME }}/routee-api:latest
docker build -t ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ inputs.environment }}-${{ github.sha }} \
-t ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ inputs.environment }}-latest .
docker push ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ inputs.environment }}-${{ github.sha }}
docker push ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ inputs.environment }}-latest

- name: Copy config files to EC2
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
host: ${{ secrets[format('{0}EC2_HOST', inputs.secret_prefix)] }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
source: "docker-compose.yml,nginx/default.conf,scripts/,tempo/,grafana/,prometheus/prometheus.yml"
Expand All @@ -51,7 +57,7 @@ jobs:
- name: Blue-Green Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
host: ${{ secrets[format('{0}EC2_HOST', inputs.secret_prefix)] }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
Expand All @@ -61,7 +67,7 @@ jobs:

export DB_HOST="${{ secrets.DB_HOST }}"
export DB_PORT="${{ secrets.DB_PORT }}"
export DB_NAME="${{ secrets.DB_NAME }}"
export DB_NAME="${{ secrets[format('{0}DB_NAME', inputs.secret_prefix)] }}"
export DB_USERNAME="${{ secrets.DB_USERNAME }}"
export DB_PASSWORD="${{ secrets.DB_PASSWORD }}"
export JWT_SECRET="${{ secrets.JWT_SECRET }}"
Expand All @@ -70,7 +76,12 @@ jobs:
export GRAFANA_ADMIN_USER="${{ secrets.GRAFANA_ADMIN_USER }}"
export GRAFANA_ADMIN_PASSWORD="${{ secrets.GRAFANA_ADMIN_PASSWORD }}"
export S3_REGION="${{ secrets.S3_REGION }}"
export S3_BUCKET="${{ secrets.S3_BUCKET }}"
export S3_ENDPOINT="${{ secrets.S3_ENDPOINT }}"
export S3_BUCKET="${{ secrets[format('{0}S3_BUCKET', inputs.secret_prefix)] }}"
export S3_ENDPOINT="${{ secrets[format('{0}S3_ENDPOINT', inputs.secret_prefix)] }}"
export APPLE_CLIENT_ID="${{ secrets[format('{0}APPLE_CLIENT_ID', inputs.secret_prefix)] }}"
export APPLE_TEAM_ID="${{ secrets[format('{0}APPLE_TEAM_ID', inputs.secret_prefix)] }}"
export APPLE_KEY_ID="${{ secrets[format('{0}APPLE_KEY_ID', inputs.secret_prefix)] }}"
export APPLE_PRIVATE_KEY="${{ secrets[format('{0}APPLE_PRIVATE_KEY', inputs.secret_prefix)] }}"
export GOOGLE_CLIENT_ID="${{ secrets.GOOGLE_CLIENT_ID }}"

cd ~/app && bash scripts/deploy.sh ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ github.sha }}
cd ~/app && bash scripts/deploy.sh ${{ secrets.DOCKER_USERNAME }}/routee-api:${{ inputs.environment }}-${{ github.sha }}
13 changes: 13 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Deploy to Production

on:
push:
branches:
- main

jobs:
deploy:
uses: ./.github/workflows/deploy-common.yml
with:
environment: production
secrets: inherit
14 changes: 14 additions & 0 deletions .github/workflows/deploy-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Deploy to Test

on:
push:
branches:
- develop

jobs:
deploy:
uses: ./.github/workflows/deploy-common.yml
with:
environment: test
secret_prefix: "TEST_"
secrets: inherit
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ services:
S3_REGION: ${S3_REGION}
S3_BUCKET: ${S3_BUCKET}
S3_ENDPOINT: ${S3_ENDPOINT}
APPLE_CLIENT_ID: ${APPLE_CLIENT_ID}
APPLE_TEAM_ID: ${APPLE_TEAM_ID}
APPLE_KEY_ID: ${APPLE_KEY_ID}
APPLE_PRIVATE_KEY: ${APPLE_PRIVATE_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
depends_on:
redis:
condition: service_healthy
Expand Down Expand Up @@ -57,6 +62,11 @@ services:
S3_REGION: ${S3_REGION}
S3_BUCKET: ${S3_BUCKET}
S3_ENDPOINT: ${S3_ENDPOINT}
APPLE_CLIENT_ID: ${APPLE_CLIENT_ID}
APPLE_TEAM_ID: ${APPLE_TEAM_ID}
APPLE_KEY_ID: ${APPLE_KEY_ID}
APPLE_PRIVATE_KEY: ${APPLE_PRIVATE_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
depends_on:
redis:
condition: service_healthy
Expand Down
1 change: 1 addition & 0 deletions routee-activity/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ dependencies {

// Modulith
compileOnly 'org.springframework.modulith:spring-modulith-api'
implementation 'org.springframework.modulith:spring-modulith-events-api'
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ public enum ErrorCode implements ErrorResultCode {
UNSUPPORTED_IMAGE_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 파일 확장자입니다."),

ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "활동 기록이 존재하지 않습니다."),
TIMELINE_NOT_FOUND(HttpStatus.NOT_FOUND, "타임라인이 존재하지 않습니다."),

ACTIVITY_ALREADY_COMPLETED(HttpStatus.CONFLICT, "이미 완료된 활동입니다."),
ACTIVITY_STATUS_ALREADY_SAME(HttpStatus.CONFLICT, "이미 동일한 활동 상태입니다."),
ALREADY_IN_PROGRESS_ACTIVITY(HttpStatus.CONFLICT, "이미 진행 중이거나 일시정지된 활동이 있습니다."),
INVALID_ACTIVITY_STATUS_TRANSITION(HttpStatus.CONFLICT, "변경할 수 없는 활동 상태입니다."),
ROUTE_ALREADY_EXISTS(HttpStatus.CONFLICT, "루트 목록이 이미 존재합니다.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public enum SuccessCode implements SuccessResultCode {
ARCHIVE_ACTIVITY_LIST_GET_SUCCESS(HttpStatus.OK, "특정 날짜의 활동 목록 조회에 성공했습니다."),
IMAGE_UPLOAD_URL_CREATED(HttpStatus.OK, "이미지 업로드 URL 발급에 성공했습니다."),
ROUTE_LIST_GET_SUCCESS(HttpStatus.OK, "루트 목록 조회에 성공했습니다."),
TIMELINE_DELETED(HttpStatus.OK, "타임라인 삭제에 성공했습니다."),
TIMELINE_LIST_GET_SUCCESS(HttpStatus.OK, "타임라인 목록 조회에 성공했습니다."),
TIMELINE_TITLE_UPDATED(HttpStatus.OK, "타임라인 제목 수정에 성공했습니다."),

ACTIVITY_CREATED(HttpStatus.CREATED, "활동 기록 생성에 성공했습니다."),
ROUTE_LIST_CREATE_SUCCESS(HttpStatus.CREATED, "루트 목록 생성에 성공했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,7 @@ public interface ActivityControllerDocs {
value = "{\"status\":400,\"code\":\"INVALID_TIME_ZONE\",\"message\":\"Time-Zone 헤더 값이 올바르지 않습니다.\"}")
})),
@ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "409", description = "이미 진행 중인 활동이 있음",
content = @Content(schema = @Schema(implementation = FailureResponse.class),
examples = @ExampleObject(name = "ALREADY_IN_PROGRESS_ACTIVITY",
value = "{\"status\":409,\"code\":\"ALREADY_IN_PROGRESS_ACTIVITY\",\"message\":\"이미 진행 중이거나 일시정지된 활동이 있습니다.\"}")))
content = @Content(schema = @Schema(implementation = FailureResponse.class)))
})
ResponseEntity<SuccessResponse<ActivityCreateResponse>> create(
Long memberId,
Expand Down Expand Up @@ -256,7 +252,7 @@ ResponseEntity<SuccessResponse<Void>> complete(
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true,
content = @Content(schema = @Schema(implementation = ActivityCompleteRequest.class),
examples = @ExampleObject(value = """
{"title":"북한산 기록","distance":5400,"durationSec":3600,"maxElevation":836,"mapImageUrl":"https://example.com/map.png","coverImageObjectKey":"activity/1/cover.png","track":[{"latitude":37.566,"longitude":126.978,"elevation":20,"pointIndex":0},{"latitude":37.567,"longitude":126.979,"elevation":25,"pointIndex":10}],"endedAt":"2026-07-07T16:30:00"}""")))
{"title":"북한산 기록","distance":5400,"durationSec":3600,"maxElevation":836,"mapImageObjectKey":"a1b2c3d4e5f6.jpg","track":[{"latitude":37.566,"longitude":126.978,"elevation":20,"pointIndex":0},{"latitude":37.567,"longitude":126.979,"elevation":25,"pointIndex":10}],"endedAt":"2026-07-07T16:30:00"}""")))
@Valid @RequestBody ActivityCompleteRequest request
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public interface RouteControllerDocs {
@ExampleObject(name = "ROUTES_EMPTY",
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"루트 목록 생성 시 최소 1개 이상의 루트를 입력해야 합니다.\"}"),
@ExampleObject(name = "ROUTES_TOO_MANY",
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"루트는 최대 12개까지 등록할 수 있습니다.\"}")
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"루트는 최대 20개까지 등록할 수 있습니다.\"}"),
@ExampleObject(name = "ROUTE_NAME_TOO_LONG",
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"name은 16자 이하여야 합니다.\"}")
})),
@ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@

import org.sopt.routee.activity.internal.code.SuccessCode;
import org.sopt.routee.activity.internal.controller.dto.request.CreateTimelineRequest;
import org.sopt.routee.activity.internal.controller.dto.request.TimelineTitleUpdateRequest;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineCreateResponse;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineListResponse;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineTitleResponse;
import org.sopt.routee.activity.internal.service.TimelineService;
import org.sopt.routee.activity.internal.service.dto.result.CreateTimelineResult;
import org.sopt.routee.activity.internal.service.dto.result.TimelineResult;
import org.sopt.routee.activity.internal.service.dto.result.UpdateTimelineTitleResult;
import org.sopt.routee.response.ApiResponse;
import org.sopt.routee.response.SuccessResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -32,16 +39,16 @@ public class TimelineController implements TimelineControllerDocs {
private final TimelineService timelineService;

@PostMapping("/{activityId}/timeline")
public ResponseEntity<SuccessResponse<Void>> create(
public ResponseEntity<SuccessResponse<TimelineCreateResponse>> create(
@AuthenticationPrincipal Long memberId,
@PathVariable(name = "activityId") Long activityId,
@RequestHeader("Time-Zone") ZoneId timeZone,
@Valid @RequestBody CreateTimelineRequest request
) {
timelineService.create(request.toCommand(memberId, activityId, timeZone));
CreateTimelineResult result = timelineService.create(request.toCommand(memberId, activityId, timeZone));

return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(SuccessCode.TIMELINE_CREATED));
.body(ApiResponse.success(SuccessCode.TIMELINE_CREATED, TimelineCreateResponse.from(result)));
}

@GetMapping("/{activityId}/timeline")
Expand All @@ -54,4 +61,27 @@ public ResponseEntity<SuccessResponse<TimelineListResponse>> getTimelines(
return ResponseEntity.ok(
ApiResponse.success(SuccessCode.TIMELINE_LIST_GET_SUCCESS, TimelineListResponse.of(activityId, results)));
}

@PatchMapping("/{activityId}/timeline/{timelineId}")
public ResponseEntity<SuccessResponse<TimelineTitleResponse>> updateTitle(
@AuthenticationPrincipal Long memberId,
@PathVariable(name = "activityId") Long activityId,
@PathVariable(name = "timelineId") Long timelineId,
@Valid @RequestBody TimelineTitleUpdateRequest request
) {
UpdateTimelineTitleResult result = timelineService.updateTitle(request.toCommand(activityId, timelineId, memberId));

return ResponseEntity.ok(ApiResponse.success(SuccessCode.TIMELINE_TITLE_UPDATED, TimelineTitleResponse.from(result)));
}

@DeleteMapping("/{activityId}/timeline/{timelineId}")
public ResponseEntity<SuccessResponse<Void>> delete(
@AuthenticationPrincipal Long memberId,
@PathVariable(name = "activityId") Long activityId,
@PathVariable(name = "timelineId") Long timelineId
) {
timelineService.delete(activityId, timelineId, memberId);

return ResponseEntity.ok(ApiResponse.success(SuccessCode.TIMELINE_DELETED));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import java.time.ZoneId;

import org.sopt.routee.activity.internal.controller.dto.request.CreateTimelineRequest;
import org.sopt.routee.activity.internal.controller.dto.request.TimelineTitleUpdateRequest;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineCreateResponse;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineListResponse;
import org.sopt.routee.activity.internal.controller.dto.response.TimelineTitleResponse;
import org.sopt.routee.response.FailureResponse;
import org.sopt.routee.response.SuccessResponse;
import org.springframework.http.ResponseEntity;
Expand All @@ -29,8 +32,8 @@ public interface TimelineControllerDocs {
@Operation(summary = "타임라인 생성", description = "인증된 사용자의 활동 기록에 타임라인을 생성합니다.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "타임라인 생성 성공",
content = @Content(
examples = @ExampleObject(value = "{\"status\":201,\"code\":\"TIMELINE_CREATED\",\"message\":\"타임라인 생성에 성공했습니다.\",\"data\":null}"))),
content = @Content(schema = @Schema(implementation = TimelineCreateResponse.class),
examples = @ExampleObject(value = "{\"status\":201,\"code\":\"TIMELINE_CREATED\",\"message\":\"타임라인 생성에 성공했습니다.\",\"data\":{\"timelineId\":3}}"))),
@ApiResponse(responseCode = "400", description = "요청 값이 올바르지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class),
examples = {
Expand All @@ -46,7 +49,7 @@ public interface TimelineControllerDocs {
examples = @ExampleObject(name = "ACTIVITY_NOT_FOUND",
value = "{\"status\":404,\"code\":\"ACTIVITY_NOT_FOUND\",\"message\":\"활동 기록이 존재하지 않습니다.\"}")))
})
ResponseEntity<SuccessResponse<Void>> create(
ResponseEntity<SuccessResponse<TimelineCreateResponse>> create(
@Parameter(hidden = true)
Long memberId,
@Parameter(description = "활동 기록 식별자", example = "1", required = true)
Expand Down Expand Up @@ -76,4 +79,66 @@ ResponseEntity<SuccessResponse<TimelineListResponse>> getTimelines(
@Parameter(description = "활동 기록 식별자", example = "1", required = true)
@PathVariable(name = "activityId") Long activityId
);

@Operation(summary = "타임라인 제목 수정", description = "인증된 사용자의 활동 기록에 속한 타임라인 제목을 수정합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "타임라인 제목 수정 성공",
content = @Content(schema = @Schema(implementation = TimelineTitleResponse.class),
examples = @ExampleObject(value = "{\"status\":200,\"code\":\"TIMELINE_TITLE_UPDATED\",\"message\":\"타임라인 제목 수정에 성공했습니다.\",\"data\":{\"timelineId\":3,\"title\":\"백운대 정상 도착\"}}"))),
@ApiResponse(responseCode = "400", description = "요청 값이 올바르지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class),
examples = {
@ExampleObject(name = "TITLE_REQUIRED",
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"title은 필수입니다.\"}"),
@ExampleObject(name = "TITLE_TOO_LONG",
value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"title은 16자 이하여야 합니다.\"}")
})),
@ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "404", description = "활동 기록 또는 타임라인이 존재하지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class),
examples = {
@ExampleObject(name = "ACTIVITY_NOT_FOUND",
value = "{\"status\":404,\"code\":\"ACTIVITY_NOT_FOUND\",\"message\":\"활동 기록이 존재하지 않습니다.\"}"),
@ExampleObject(name = "TIMELINE_NOT_FOUND",
value = "{\"status\":404,\"code\":\"TIMELINE_NOT_FOUND\",\"message\":\"타임라인이 존재하지 않습니다.\"}")
}))
})
ResponseEntity<SuccessResponse<TimelineTitleResponse>> updateTitle(
@Parameter(hidden = true)
Long memberId,
@Parameter(description = "활동 기록 식별자", example = "1", required = true)
@PathVariable(name = "activityId") Long activityId,
@Parameter(description = "타임라인 식별자", example = "3", required = true)
@PathVariable(name = "timelineId") Long timelineId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true,
content = @Content(schema = @Schema(implementation = TimelineTitleUpdateRequest.class),
examples = @ExampleObject(value = "{\"title\":\"백운대 정상 도착\"}")))
@Valid @RequestBody TimelineTitleUpdateRequest request
);

@Operation(summary = "타임라인 삭제", description = "인증된 사용자의 활동 기록에 속한 타임라인을 삭제합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "타임라인 삭제 성공",
content = @Content(
examples = @ExampleObject(value = "{\"status\":200,\"code\":\"TIMELINE_DELETED\",\"message\":\"타임라인 삭제에 성공했습니다.\",\"data\":null}"))),
@ApiResponse(responseCode = "401", description = "인증 실패",
content = @Content(schema = @Schema(implementation = FailureResponse.class))),
@ApiResponse(responseCode = "404", description = "활동 기록 또는 타임라인이 존재하지 않음",
content = @Content(schema = @Schema(implementation = FailureResponse.class),
examples = {
@ExampleObject(name = "ACTIVITY_NOT_FOUND",
value = "{\"status\":404,\"code\":\"ACTIVITY_NOT_FOUND\",\"message\":\"활동 기록이 존재하지 않습니다.\"}"),
@ExampleObject(name = "TIMELINE_NOT_FOUND",
value = "{\"status\":404,\"code\":\"TIMELINE_NOT_FOUND\",\"message\":\"타임라인이 존재하지 않습니다.\"}")
}))
})
ResponseEntity<SuccessResponse<Void>> delete(
@Parameter(hidden = true)
Long memberId,
@Parameter(description = "활동 기록 식별자", example = "1", required = true)
@PathVariable(name = "activityId") Long activityId,
@Parameter(description = "타임라인 식별자", example = "3", required = true)
@PathVariable(name = "timelineId") Long timelineId
);
}
Loading
Loading