Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5bdc95e
feat: 사용자 언어 변경 기능 구현
aneykrap Jul 6, 2026
d7b5453
fix: 충돌 해결
aneykrap Jul 7, 2026
1f69e38
refactor: 불필요한 공백 제거
aneykrap Jul 6, 2026
387248d
fix: 충돌 해결
aneykrap Jul 7, 2026
3cb4149
fix: Swagger 문서에 보안 요구 사항 추가 및 불필요한 코드 정리
aneykrap Jul 7, 2026
06e58f7
feat: ReissueResult DTO 추가
Jy000n Jul 6, 2026
cddb4dd
feat: AuthService에 재발급 비즈니스 로직 추가
Jy000n Jul 6, 2026
8bfe4a4
refactor: Auth 응답 코드 네이밍 수정
Jy000n Jul 6, 2026
fa3a27b
feat: 쿠키 생성/만료 공통 유틸 CookieUtil 추가
Jy000n Jul 6, 2026
83db291
refactor: OAuthSuccessHandler 쿠키 생성 로직을 CookieUtil로 변경
Jy000n Jul 6, 2026
cc7a2f8
feat: AccessToken 재발급 API 구현 (refreshToken rotation 적용)
Jy000n Jul 6, 2026
b576c10
feat: 로그아웃 API 구현
Jy000n Jul 6, 2026
81fce48
feat: 회원 탈퇴 API 구현
Jy000n Jul 6, 2026
b96407f
refactor: Auth API 경로 변경
Jy000n Jul 6, 2026
f34eeb4
feat: 로그아웃된 accessToken 블랙리스트 관리를 위한 BlacklistService 추가
Jy000n Jul 6, 2026
989d4ab
feat: JwtTokenProvider에 getRemainingExpiry 메서드 추가
Jy000n Jul 6, 2026
d2f11a6
feat: JwtAuthenticationFilter에 블랙리스트 검증 추가
Jy000n Jul 6, 2026
9424cbf
feat: 로그아웃 시 Access Token 블랙리스트 처리 추가
Jy000n Jul 6, 2026
26ac5ed
feat: 회원 탈퇴 시 Access Token 블랙리스트 처리 추가
Jy000n Jul 6, 2026
444f68f
fix: JwtAuthenticationFilter 블랙리스트 검증 로직 수정
Jy000n Jul 6, 2026
a24a68e
refactor: resolveToken을 TokenExtractor 유틸로 분리하여 중복 제거
Jy000n Jul 6, 2026
23efe1c
chore: 불필요한 import 및 공백 제거
Jy000n Jul 6, 2026
c5d7457
fix: 쿠키 path를 /api/v1/auth로 수정
Jy000n Jul 6, 2026
b084b58
fix: 탈퇴 API 경로 변경
Jy000n Jul 6, 2026
d8d0791
refactor: 탈퇴 로직에서 사용자 존재 검증 순서 변경
Jy000n Jul 6, 2026
1c8ea8b
refactor: 탈퇴 로직 트랜잭션 적용
Jy000n Jul 6, 2026
81b2eae
fix: 만료된 Access Token은 블랙리스트에 저장하지 않도록 수정
Jy000n Jul 6, 2026
493234d
refactor: Controller에서 Swagger docs 인터페이스 분리 적용
Jy000n Jul 6, 2026
d326e90
fix: createdAt JSON 직렬화 포맷 설정
Jy000n Jul 6, 2026
29ebb29
fix: AuthControllerDocs token 메서드 파라미터 타입 불일치 수정
Jy000n Jul 6, 2026
6bf7033
fix: ErrorDto timestamp Swagger 예시 포맷 수정
Jy000n Jul 6, 2026
1ed8111
refactor: Swagger Auth API 문서 개선
Jy000n Jul 6, 2026
90f5a27
refactor: AuthReissueResponse 생성
Jy000n Jul 6, 2026
73db157
feat: code 필드에 입력 검증 어노테이션 추가
Jy000n Jul 6, 2026
c6c3022
refactor: 쿠키/응답 조립 로직을 AuthResponseFactory로 분리
Jy000n Jul 7, 2026
b6d314f
fix: token 요청에 @Valid 누락 수정
Jy000n Jul 7, 2026
12be0e2
fix: 탈퇴 시 전체 세션 refresh token 삭제 및 재발급 시 유저 존재 검증 추가
Jy000n Jul 7, 2026
6ebaea8
chore: AuthController의 미사용 cookieSecure 필드 제거
Jy000n Jul 7, 2026
a71066f
feat(todo): 투두 조회용 엔티티 보강 (타이머 상태·완료 여부 등)
laura-jung Jul 8, 2026
a430a6e
feat(user): 사용자별 시간대(zoneId) 설정 기능 추가
laura-jung Jul 8, 2026
7c0589a
feat: UTC 시간대 기준 통일
laura-jung Jul 8, 2026
85c2a7a
feat: 사용자별 시간대 설정 APT 문서/DTO 보강
laura-jung Jul 8, 2026
e0cff25
feat(home): 홈화면 조회 기능
laura-jung Jul 8, 2026
3c6a918
Merge remote-tracking branch 'origin/develop' into feat/#30-getTodo
laura-jung Jul 8, 2026
e60b7ec
refactor(home): filter 빈값으로 들어올 시 defualt 반환으로 수정
laura-jung Jul 8, 2026
9381897
refactor(tag): 태그 이름 필수 값 설정
laura-jung Jul 8, 2026
03af13c
fix: UTC 시간대 설정을 main()으로 이동해 앱 시작 전 보장
laura-jung Jul 8, 2026
a0d10aa
refactor: 중복 어노테이션 @SecurityRequirement(name = "bearerAuth") 제거
laura-jung Jul 8, 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
3 changes: 3 additions & 0 deletions src/main/java/com/Timo/Timo/TimoApplication.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.Timo.Timo;

import java.util.TimeZone;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
Expand All @@ -9,6 +11,7 @@
public class TimoApplication {

public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
SpringApplication.run(TimoApplication.class, args);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.Timo.Timo.domain.home.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.Timo.Timo.domain.home.docs.HomeControllerDocs;
import com.Timo.Timo.domain.home.dto.response.HomeResponse;
import com.Timo.Timo.domain.home.exception.HomeSuccessCode;
import com.Timo.Timo.domain.home.service.HomeService;
import com.Timo.Timo.global.auth.principal.CustomUserDetails;
import com.Timo.Timo.global.response.BaseResponse;

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/v1/home")
@RequiredArgsConstructor
@Tag(name = "Home", description = "HOME API")
public class HomeController implements HomeControllerDocs {

private final HomeService homeService;

@Override
@GetMapping
public ResponseEntity<BaseResponse<HomeResponse>> getHome(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam(required = false) String filter,
@RequestParam(required = false) String baseDate
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) {
HomeResponse response = homeService.getHome(userDetails.getUserId(), filter, baseDate);

return ResponseEntity
.status(HomeSuccessCode.GET_HOME.getHttpStatus())
.body(BaseResponse.onSuccess(HomeSuccessCode.GET_HOME, response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.Timo.Timo.domain.home.docs;

import org.springframework.http.ResponseEntity;

import com.Timo.Timo.domain.home.dto.response.HomeResponse;
import com.Timo.Timo.global.auth.principal.CustomUserDetails;
import com.Timo.Timo.global.exception.dto.ErrorDto;
import com.Timo.Timo.global.response.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

public interface HomeControllerDocs {

@Operation(
summary = "HOME 화면 TODO 목록 조회",
description = """
HOME 화면의 필터 조건에 따라 일별 TODO 목록을 조회합니다.
DEFAULT는 기준 날짜 전후 7일, WEEK는 기준 날짜부터 7일을 반환합니다.
각 날짜의 TODO는 미완료 먼저, 같은 완료 그룹 안에서는 sortOrder 오름차순으로 정렬됩니다.
"""
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "홈 뷰 조회 성공",
useReturnTypeSchema = true
),
@ApiResponse(
responseCode = "400",
description = "유효하지 않은 필터 값이거나 날짜 형식이 올바르지 않은 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorDto.class)
)
),
@ApiResponse(
responseCode = "401",
description = "Access Token이 없거나 만료되었거나 유효하지 않은 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorDto.class)
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorDto.class)
)
)
})
ResponseEntity<BaseResponse<HomeResponse>> getHome(
@Parameter(hidden = true) CustomUserDetails userDetails,
@Parameter(description = "조회 필터(DEFAULT, WEEK)", example = "DEFAULT") String filter,
@Parameter(description = "기준 날짜(yyyy-MM-dd), 미입력 시 오늘", example = "2026-07-22") String baseDate
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.Timo.Timo.domain.home.dto.response;

import java.time.LocalDate;
import java.util.List;
import java.util.Objects;

import com.Timo.Timo.domain.home.enums.HomeFilter;
import com.Timo.Timo.domain.tag.entity.Tag;
import com.Timo.Timo.domain.todo.entity.Subtask;
import com.Timo.Timo.domain.todo.enums.TodoTimerStatus;
import com.Timo.Timo.domain.todo.enums.Weekday;
import com.fasterxml.jackson.annotation.JsonProperty;

public record HomeResponse(
HomeFilter filter,
LocalDate baseDate,
List<DayResponse> days
) {

public record DayResponse(
LocalDate date,
Weekday dayOfWeek,
@JsonProperty("isHoliday")
boolean isHoliday,
@JsonProperty("isToday")
boolean isToday,
int totalCount,
int completedCount,
List<TodoResponse> todos
) {
public static DayResponse of(
LocalDate date,
boolean isHoliday,
boolean isToday,
List<TodoResponse> todos
) {
int completedCount = (int) todos.stream()
.filter(TodoResponse::completed)
.count();

return new DayResponse(
date,
Weekday.from(date.getDayOfWeek()),
isHoliday,
isToday,
todos.size(),
completedCount,
todos
);
}
}

public record TodoResponse(
Long todoId,
String icon,
String title,
boolean completed,
Integer durationSeconds,
String priority,
TagResponse tag,
boolean hasMemo,
@JsonProperty("isRepeated")
boolean isRepeated,
TodoTimerStatus timerStatus,
Integer sortOrder,
List<SubtaskResponse> subtasks
) {
}

public record TagResponse(
Long tagId,
String name
) {
public static TagResponse from(Tag tag) {
Objects.requireNonNull(tag, "tag must not be null");
return new TagResponse(tag.getId(), tag.getName());
}
}

public record SubtaskResponse(
Long subtaskId,
String content,
boolean completed
) {
public static SubtaskResponse from(Subtask subtask) {
return new SubtaskResponse(
subtask.getId(),
subtask.getContent(),
subtask.isCompleted()
);
}
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/Timo/Timo/domain/home/enums/HomeFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.Timo.Timo.domain.home.enums;

public enum HomeFilter {
DEFAULT,
WEEK
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.Timo.Timo.domain.home.exception;

import org.springframework.http.HttpStatus;

import com.Timo.Timo.global.exception.code.BaseErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum HomeErrorCode implements BaseErrorCode {

INVALID_FILTER_OR_DATE(HttpStatus.BAD_REQUEST, "HOME_400", "유효하지 않은 필터 값이거나 날짜 형식이 올바르지 않습니다."),
INVALID_DATE(HttpStatus.BAD_REQUEST, "DATE_400", "날짜 형식이 올바르지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.Timo.Timo.domain.home.exception;

import org.springframework.http.HttpStatus;

import com.Timo.Timo.global.exception.code.BaseSuccessCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum HomeSuccessCode implements BaseSuccessCode {

GET_HOME(HttpStatus.OK, "홈 뷰 조회 성공");

private final HttpStatus httpStatus;
private final String message;
}
61 changes: 61 additions & 0 deletions src/main/java/com/Timo/Timo/domain/home/mapper/HomeTodoMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.Timo.Timo.domain.home.mapper;

import java.util.List;

import org.springframework.stereotype.Component;

import com.Timo.Timo.domain.home.dto.response.HomeResponse.SubtaskResponse;
import com.Timo.Timo.domain.home.dto.response.HomeResponse.TagResponse;
import com.Timo.Timo.domain.home.dto.response.HomeResponse.TodoResponse;
import com.Timo.Timo.domain.tag.entity.Tag;
import com.Timo.Timo.domain.todo.entity.Subtask;
import com.Timo.Timo.domain.todo.entity.Todo;
import com.Timo.Timo.domain.todo.entity.TodoInstance;
import com.Timo.Timo.domain.todo.enums.RepeatType;
import com.Timo.Timo.domain.todo.enums.TodoTimerStatus;

@Component
public class HomeTodoMapper {

public TodoResponse toResponse(TodoContext context) {
Todo todo = context.todo();
TodoInstance instance = context.instance();

return new TodoResponse(
todo.getId(),
todo.getIcon() != null ? todo.getIcon().name() : null,
todo.getTitle(),
instance != null && instance.isCompleted(),
todo.getDurationSeconds(),
todo.getPriority() != null ? todo.getPriority().name() : null,
mapTag(context.tag()),
todo.getMemo() != null && !todo.getMemo().isBlank(),
todo.getRepeatType() != RepeatType.NONE,
resolveTimerStatus(instance),
instance != null ? instance.getSortOrder() : context.defaultSortOrder(),
mapSubtasks(todo.getSubtasks())
);
}

private TagResponse mapTag(Tag tag) {
return tag != null ? TagResponse.from(tag) : null;
}

private TodoTimerStatus resolveTimerStatus(TodoInstance instance) {
return instance != null ? instance.getTimerStatus() : TodoTimerStatus.STOPPED;
}

private List<SubtaskResponse> mapSubtasks(List<Subtask> subtasks) {
return subtasks.stream()
.map(SubtaskResponse::from)
.toList();
}

public record TodoContext(
Todo todo,
TodoInstance instance,
Tag tag,
int defaultSortOrder
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.Timo.Timo.domain.home.service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.MonthDay;
import java.util.Map;
import java.util.Set;

import org.springframework.stereotype.Component;

import com.Timo.Timo.domain.user.enums.Language;

@Component
public class HolidayChecker {

private static final Set<MonthDay> KOREAN_FIXED_HOLIDAYS = Set.of(
MonthDay.of(1, 1),
MonthDay.of(3, 1),
MonthDay.of(5, 5),
MonthDay.of(6, 6),
MonthDay.of(8, 15),
MonthDay.of(10, 3),
MonthDay.of(10, 9),
MonthDay.of(12, 25)
);

/**
* 언어(지역)별 양력 고정 공휴일. 음력 공휴일(설날·추석 등)은 별도 계산이 필요해 아직 포함하지 않는다.
* 신뢰할 만한 데이터가 없는 언어는 빈 집합으로 두고 일요일만 공휴일로 처리한다.
*/
private static final Map<Language, Set<MonthDay>> FIXED_HOLIDAYS_BY_LANGUAGE = Map.of(
Language.KO, KOREAN_FIXED_HOLIDAYS,
Language.EN, Set.of()
);

public boolean isHoliday(LocalDate date, Language language) {
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
return true;
}

Set<MonthDay> fixedHolidays = FIXED_HOLIDAYS_BY_LANGUAGE.getOrDefault(language, Set.of());
return fixedHolidays.contains(MonthDay.from(date));
}
}
Loading