Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.project.growfit.domain.Goal.controller;

import com.project.growfit.domain.Goal.dto.request.CreateWeeklyGoalRequestDto;
import com.project.growfit.domain.Goal.dto.request.GoalCertificationRequestDto;
import com.project.growfit.domain.Goal.dto.response.CertificationResponseDto;
import com.project.growfit.domain.Goal.dto.response.GoalResponseDto;
import com.project.growfit.domain.Goal.dto.response.WeeklyGoalResponseDto;
import com.project.growfit.domain.Goal.service.GoalService;
import com.project.growfit.global.response.ResultCode;
import com.project.growfit.global.response.ResultResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

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

@Validated
@RestController
@RequestMapping("/api/goal")
@RequiredArgsConstructor
@Tag(name = "주간 목표 API", description = "주간 목표 및 일일 인증 관련 API입니다.")
public class GoalController {

private final GoalService goalService;

@PostMapping
@Operation(summary = "주간 목표 생성", description = "부모가 주간 목표(WeeklyGoal)를 생성합니다. 시작일, 종료일, 인증 횟수, 목표 리스트를 포함해야 합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "주간 목표 생성 성공"),
@ApiResponse(responseCode = "400", description = "요청 형식 오류 또는 중복된 주간 목표 존재"),
@ApiResponse(responseCode = "403", description = "권한 없음")})
public ResultResponse<WeeklyGoalResponseDto> createWeeklyGoal(@Valid @RequestBody CreateWeeklyGoalRequestDto request) {
WeeklyGoalResponseDto dto = goalService.createWeeklyGoal(request);

return ResultResponse.of(ResultCode.WEEKLY_GOAL_CREATE_SUCCESS, dto);
}

/* 주간 목표 단일 조회 API (날짜 기준) */
@GetMapping
@Operation(summary = "주간 목표 단일 조회", description = "특정 날짜가 속한 주간 목표를 조회합니다. " + "인증된 부모의 목표만 조회 가능합니다.")
@Parameters({@Parameter(name = "date", description = "조회할 날짜 (yyyy-MM-dd)", required = true, example = "2025-06-24")})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "주간 목표 조회 성공"),
@ApiResponse(responseCode = "404", description = "해당 날짜에 해당하는 주간 목표가 존재하지 않음")})
public ResultResponse<WeeklyGoalResponseDto> getWeeklyGoal(@RequestParam("date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
WeeklyGoalResponseDto dto = goalService.getWeeklyGoal(date);

return ResultResponse.of(ResultCode.WEEKLY_GOAL_FETCH_SUCCESS, dto);
}

@PostMapping(value = "/{goalId}/certify", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "일일 목표 인증", description = "아이 계정으로 특정 목표에 대한 인증 이미지를 업로드합니다. " + "하루에 한 번만 인증 가능합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "목표 인증 성공"),
@ApiResponse(responseCode = "400", description = "이미 오늘 인증한 경우"),
@ApiResponse(responseCode = "403", description = "권한 없음"),
@ApiResponse(responseCode = "404", description = "해당 목표 ID가 존재하지 않음")})
public ResultResponse<GoalResponseDto> certifyDailyGoal(@PathVariable Long goalId,
@RequestPart(name = "image", required = false) MultipartFile image) {
GoalResponseDto dto = goalService.certifyDailyGoal(goalId, image);

return ResultResponse.of(ResultCode.GOAL_CERTIFICATION_SUCCESS, dto);
}

@GetMapping("/{goalId}/certifications")
@Operation(summary = "목표 인증 내역 조회", description = "특정 목표(goalId)에 등록된 인증 내역 리스트를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "인증 내역 조회 성공"),
@ApiResponse(responseCode = "404", description = "해당 목표가 존재하지 않음")})
public ResultResponse<List<CertificationResponseDto>> getCertifications(@PathVariable Long goalId) {
List<CertificationResponseDto> dto = goalService.getCertificationsByGoalId(goalId);

return ResultResponse.of(ResultCode.CERTIFICATION_LIST_FETCH_SUCCESS, dto);
}

@PutMapping("/{goalId}")
@Operation(summary = "목표 수정", description = "특정 목표(goalId) 타이틀(제목)을 수정합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "인증 내역 조회 성공"),
@ApiResponse(responseCode = "400", description = "이미 인증을 진행중인 목표는 수정 불가"),
@ApiResponse(responseCode = "404", description = "해당 목표가 존재하지 않음")})
public ResultResponse<GoalResponseDto> getCertifications(@PathVariable Long goalId,
@Valid @NotBlank @RequestParam String title) {
GoalResponseDto dto = goalService.updateGoalTitle(goalId, title);

return ResultResponse.of(ResultCode.GOAL_UPDATE_SUCCESS, dto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.project.growfit.domain.Goal.controller;

import com.project.growfit.domain.Goal.dto.request.LetterRequestDto;
import com.project.growfit.domain.Goal.dto.response.LetterBasicResponseDto;
import com.project.growfit.domain.Goal.dto.response.LetterResponseDto;
import com.project.growfit.domain.Goal.service.LetterService;
import com.project.growfit.global.response.ResultCode;
import com.project.growfit.global.response.ResultResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/letter")
@RequiredArgsConstructor
@Tag(name = "칭찬 편지 API", description = "주간 목표가 완료되면 부모가 작성하고 아이가 조회할 수 있는 편지 관련 API입니다.")
public class LetterController {

private final LetterService letterService;

@PostMapping("/{weeklyGoalId}")
@Operation(summary = "편지 작성", description = "모든 목표가 COMPLETE 상태일 때 편지를 작성할 수 있습니다.")
public ResultResponse<LetterBasicResponseDto> createLetter(@PathVariable Long weeklyGoalId,
@RequestBody @Valid LetterRequestDto request) {
LetterBasicResponseDto dto = letterService.createLetter(weeklyGoalId, request);
return ResultResponse.of(ResultCode.LETTER_CREATE_SUCCESS, dto);
}

@GetMapping("/{weeklyGoalId}")
@Operation(summary = "편지 조회", description = "아이 또는 부모가 주간 목표에 작성된 편지를 조회합니다.")
public ResultResponse<LetterResponseDto> getLetter(@PathVariable Long weeklyGoalId) {
LetterResponseDto dto = letterService.getLetterByWeeklyGoalId(weeklyGoalId);
return ResultResponse.of(ResultCode.LETTER_FETCH_SUCCESS, dto);
}

@GetMapping
@Operation(summary = "모든 편지 페이징 조회", description = "부모가 지금까지 작성한 모든 편지를 페이징으로 조회합니다.")
public ResultResponse<Page<LetterBasicResponseDto> > getAllLetters(@Parameter(description = "페이지 번호", example = "0") @RequestParam(defaultValue = "0") @Min(0) int page,
@Parameter(description = "페이지 크기", example = "10") @RequestParam(defaultValue = "10") @Min(1) int size){
Page<LetterBasicResponseDto> dto = letterService.getAllLetters(page, size);
return ResultResponse.of(ResultCode.LETTER_LIST_FETCH_SUCCESS, dto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.project.growfit.domain.Goal.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;

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

@Schema(description = "주간 목표 생성 요청 DTO")
public record CreateWeeklyGoalRequestDto(
@NotNull
@Schema(description = "시작 날짜", example = "2025-06-24")
LocalDate startDate,

@NotNull
@Schema(description = "종료 날짜", example = "2025-06-30")
LocalDate endDate,

@NotNull
@Min(value = 1, message = "인증 횟수는 최소 1회 이상이어야 합니다.")
@Max(value = 7, message = "인증 횟수는 최대 7회까지 가능합니다.")
@Schema(description = "목표 달성 설정 갯수", example = "2")
int certificationCount,

@Valid
@Size(min = 1, max = 10, message = "목표는 최소 1개 이상, 최대 10개까지 설정 가능합니다.")
@NotEmpty(message = "목표 목록은 비어 있을 수 없습니다.")
@Schema(description = "목표 목록")
List<GoalItem> goals
) {
public record GoalItem(
@NotNull @Schema(description = "목표 이름", example = "매일 독서 20분")
String name,

@NotNull @Schema(description = "아이콘 ID", example = "1")
int iconId
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.project.growfit.domain.Goal.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(description = "목표 인증 요청 DTO")
public record GoalCertificationRequestDto(
@NotNull @Schema(description = "이미지 URL", example = "https://s3-url/image.jpg")
String imageUrl
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.project.growfit.domain.Goal.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "칭찬 편지 작성 요청 DTO")
public record LetterRequestDto(

@Schema(description = "편지 내용", example = "이번 주 목표를 모두 완성했어요! 정말 자랑스럽고 대견해요 :)")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.Certification;

import java.time.LocalDateTime;

public record CertificationResponseDto(
Long id,
String imageUrl,
LocalDateTime certifiedAt
) {
public static CertificationResponseDto toDto(Certification certification) {
return new CertificationResponseDto(
certification.getId(),
certification.getImageUrl(),
certification.getCertifiedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.Goal;
import com.project.growfit.domain.Goal.entity.GoalStatus;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "개별 목표 응답 DTO")
public record GoalResponseDto(
Long goalId,
String name,
int iconId,
int certifiedCount,
GoalStatus status
) {
public static GoalResponseDto toDto(Goal goal) {
return new GoalResponseDto(
goal.getId(),
goal.getName(),
goal.getIconId(),
goal.getCertificationList().size(),
goal.getStatus()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.Letter;
import com.project.growfit.domain.Goal.entity.WeeklyGoal;

import java.util.List;

public record LetterBasicResponseDto(
Long letterId,
Long weeklyGoalId,
Long parentId,
String content
) {
public static LetterBasicResponseDto toDto(Letter letter) {
return new LetterBasicResponseDto(
letter.getId(),
letter.getWeeklyGoal().getId(),
letter.getWeeklyGoal().getParent().getId(),
letter.getContent()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.Letter;

import java.util.List;
import java.util.stream.Collectors;

public record LetterResponseDto(
Long letterId,
String content,
List<LetterDetailDto> details
) {
public static LetterResponseDto toDto(Letter letter) {
List<LetterDetailDto> detailDtos = letter.getWeeklyGoal().getGoalList().stream()
.filter(goal -> !goal.getCertificationList().isEmpty())
.map(goal -> new LetterDetailDto(
goal.getId(),
goal.getCertificationList().get(0).getImageUrl()
))
.collect(Collectors.toList());

return new LetterResponseDto(
letter.getId(),
letter.getContent(),
detailDtos
);
}

public record LetterDetailDto(
Long goalId,
String imageUrl
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.GoalStatus;
import com.project.growfit.domain.Goal.entity.WeeklyGoal;
import io.swagger.v3.oas.annotations.media.Schema;

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

@Schema(description = "주간 달성 이력 응답 DTO")
public record WeeklyGoalHistoryResponseDto(
LocalDate startDate,
LocalDate endDate,
List<GoalHistory> goals
) {
public record GoalHistory(
String name,
GoalStatus status,
int totalDays,
int certifiedDays
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.project.growfit.domain.Goal.dto.response;

import com.project.growfit.domain.Goal.entity.GoalStatus;
import com.project.growfit.domain.Goal.entity.WeeklyGoal;
import io.swagger.v3.oas.annotations.media.Schema;

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

@Schema(description = "주간 목표 조회 응답 DTO")
public record WeeklyGoalResponseDto(
Long weeklyGoalId,
LocalDate startDate,
LocalDate endDate,
boolean isLetterSent,
int targetCount,
List<GoalResponseDto> goals
) {
public static WeeklyGoalResponseDto toDto(WeeklyGoal weeklyGoal) {
List<GoalResponseDto> goals =
weeklyGoal.getGoalList().stream().map(
goal -> new GoalResponseDto(
goal.getId(),
goal.getName(),
goal.getIconId(),
goal.getCertificationList().size(),
goal.getStatus()

))
.toList();

return new WeeklyGoalResponseDto(
weeklyGoal.getId(),
weeklyGoal.getStartDate(),
weeklyGoal.getEndDate(),
weeklyGoal.isLetterSent(),
weeklyGoal.getCertificationCount(),
goals
);
}
}
Loading