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,159 @@
package com.devkor.ifive.nadab.domain.dailyreport.api;

import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.FeedListResponse;
import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.ShareStatusResponse;
import com.devkor.ifive.nadab.domain.dailyreport.application.FeedQueryService;
import com.devkor.ifive.nadab.domain.dailyreport.application.FeedService;
import com.devkor.ifive.nadab.global.core.response.ApiResponseDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity;
import com.devkor.ifive.nadab.global.security.principal.UserPrincipal;
import io.swagger.v3.oas.annotations.Operation;
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.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "피드 API", description = "친구 피드 공유 및 조회 관련 API")
@RestController
@RequestMapping("${api_prefix}/feed")
@RequiredArgsConstructor
public class FeedController {

private final FeedService feedService;
private final FeedQueryService feedQueryService;

@PostMapping("/share")
@PreAuthorize("isAuthenticated()")
@Operation(
summary = "공유 시작 API",
description = "당일 DailyReport를 친구들에게 공유합니다.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(
responseCode = "204",
description = "공유 시작 성공",
content = @Content
),
@ApiResponse(
responseCode = "401",
description = "인증 실패",
content = @Content
),
@ApiResponse(
responseCode = "404",
description = "ErrorCode: DAILY_REPORT_NOT_FOUND - 당일 리포트를 찾을 수 없음",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<Void>> startSharing(
@AuthenticationPrincipal UserPrincipal principal
) {
feedService.startSharing(principal.getId());
return ApiResponseEntity.noContent();
}

@PostMapping("/unshare")
@PreAuthorize("isAuthenticated()")
@Operation(
summary = "공유 중단 API",
description = "당일 DailyReport 공유를 중단합니다.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(
responseCode = "204",
description = "공유 중단 성공",
content = @Content
),
@ApiResponse(
responseCode = "401",
description = "인증 실패",
content = @Content
),
@ApiResponse(
responseCode = "404",
description = "ErrorCode: DAILY_REPORT_NOT_FOUND - 당일 리포트를 찾을 수 없음",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<Void>> stopSharing(
@AuthenticationPrincipal UserPrincipal principal
) {
feedService.stopSharing(principal.getId());
return ApiResponseEntity.noContent();
}

@GetMapping("/share/status")
@PreAuthorize("isAuthenticated()")
@Operation(
summary = "공유 상태 조회 API",
description = """
당일 DailyReport의 공유 상태를 조회합니다.

이 API는 상세보기 화면에서 오늘 날짜 답변일 때 공유 버튼 상태를 확인하기 위해 사용됩니다.
""",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content = @Content(
schema = @Schema(implementation = ShareStatusResponse.class),
mediaType = "application/json"
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패",
content = @Content
),
@ApiResponse(
responseCode = "404",
description = "ErrorCode: DAILY_REPORT_NOT_FOUND - 당일 리포트를 찾을 수 없음",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<ShareStatusResponse>> getShareStatus(
@AuthenticationPrincipal UserPrincipal principal
) {
ShareStatusResponse response = feedQueryService.getShareStatus(principal.getId());
return ApiResponseEntity.ok(response);
}

@GetMapping
@PreAuthorize("isAuthenticated()")
@Operation(
summary = "피드 조회 API",
description = "친구들의 공유된 DailyReport 목록을 조회합니다. ACCEPTED 상태의 친구만 조회됩니다.",
security = @SecurityRequirement(name = "bearerAuth"),
responses = {
@ApiResponse(
responseCode = "200",
description = "피드 조회 성공",
content = @Content(
schema = @Schema(implementation = FeedListResponse.class),
mediaType = "application/json"
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패",
content = @Content
)
}
)
public ResponseEntity<ApiResponseDto<FeedListResponse>> getFeeds(
@AuthenticationPrincipal UserPrincipal principal
) {
FeedListResponse response = feedQueryService.getFeeds(principal.getId());
return ApiResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public record DailyReportResponse(
String content,

@Schema(description = "오늘의 리포트 감정 상태", example = "GROWTH")
String emotion
String emotion,

@Schema(description = "피드 공유 상태", example = "false")
Boolean isShared
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

@Schema(description = "피드 목록 응답")
public record FeedListResponse(

@Schema(description = "피드 목록")
List<FeedResponse> feeds
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response;

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

@Schema(description = "피드 응답")
public record FeedResponse(

@Schema(description = "친구 닉네임", example = "모래")
String friendNickname,

@Schema(description = "친구 프로필 이미지 URL", example = "https://cdn.example.com/profiles/abc123.png")
String friendProfileImageUrl,

@Schema(description = "관심분야 코드", example = "EMOTION")
String interestCode,

@Schema(description = "질문 내용", example = "오늘 가장 기뻤던 순간은?")
String questionText,

@Schema(description = "답변 내용", example = "집에 갈 때")
String answer,

@Schema(description = "감정 코드", example = "ACHIEVEMENT")
String emotionCode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response;

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

@Schema(description = "피드 공유 상태 응답")
public record ShareStatusResponse(

@Schema(description = "오늘의 기록 공유 상태", example = "true")
Boolean isShared
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public DailyReportResponse getDailyReport(Long id) {
return new DailyReportResponse(
entry.getContent(),
report.getContent(),
report.getEmotion().getCode().toString()
report.getEmotion().getCode().toString(),
report.getIsShared()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.devkor.ifive.nadab.domain.dailyreport.application;

import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.FeedResponse;
import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.FeedListResponse;
import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.ShareStatusResponse;
import com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReport;
import com.devkor.ifive.nadab.domain.dailyreport.core.repository.DailyReportRepository;
import com.devkor.ifive.nadab.domain.dailyreport.core.dto.FeedDto;
import com.devkor.ifive.nadab.domain.friend.core.entity.Friendship;
import com.devkor.ifive.nadab.domain.friend.core.entity.FriendshipStatus;
import com.devkor.ifive.nadab.domain.friend.core.repository.FriendshipRepository;
import com.devkor.ifive.nadab.domain.user.core.entity.DefaultProfileType;
import com.devkor.ifive.nadab.domain.user.infra.ProfileImageUrlBuilder;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FeedQueryService {

private final FriendshipRepository friendshipRepository;
private final DailyReportRepository dailyReportRepository;
private final ProfileImageUrlBuilder profileImageUrlBuilder;

public FeedListResponse getFeeds(Long userId) {
// 1. ACCEPTED 상태의 친구 관계 조회
List<Friendship> friendships = friendshipRepository
.findByUserIdAndStatusWithUsers(userId, FriendshipStatus.ACCEPTED);

// 2. 친구 ID 리스트 추출
List<Long> friendIds = friendships.stream()
.map(f -> f.getOtherUserId(userId))
.toList();

// 3. 친구가 없으면 빈 리스트 반환
if (friendIds.isEmpty()) {
return new FeedListResponse(List.of());
}

// 4. 당일 공유된 피드 조회
LocalDate today = TodayDateTimeProvider.getTodayDate();
List<FeedDto> feedDtos = dailyReportRepository
.findSharedFeedsByFriendIds(today, friendIds);

// 5. 응답 DTO 변환
List<FeedResponse> feeds = feedDtos.stream()
.map(dto -> {
String profileUrl = buildProfileUrl(dto.profileImageKey(), dto.defaultProfileType());

return new FeedResponse(
dto.nickname(),
profileUrl,
dto.interestCode() != null ? dto.interestCode().name() : null,
dto.questionText(),
dto.answerContent(),
dto.emotionCode() != null ? dto.emotionCode().name() : null
);
})
.toList();

return new FeedListResponse(feeds);
}

public ShareStatusResponse getShareStatus(Long userId) {
// 1. 당일 DailyReport 조회
LocalDate today = TodayDateTimeProvider.getTodayDate();
DailyReport report = dailyReportRepository.findByUserIdAndDate(userId, today)
.orElseThrow(() -> new NotFoundException(ErrorCode.DAILY_REPORT_NOT_FOUND));

return new ShareStatusResponse(report.getIsShared());
}

private String buildProfileUrl(String profileImageKey, DefaultProfileType defaultProfileType) {
if (profileImageKey != null) {
return profileImageUrlBuilder.buildUrl(profileImageKey);
}
if (defaultProfileType != null) {
return profileImageUrlBuilder.buildDefaultUrl(defaultProfileType);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.devkor.ifive.nadab.domain.dailyreport.application;

import com.devkor.ifive.nadab.domain.dailyreport.core.entity.DailyReport;
import com.devkor.ifive.nadab.domain.dailyreport.core.repository.DailyReportRepository;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;

@Service
@RequiredArgsConstructor
@Transactional
public class FeedService {

private final DailyReportRepository dailyReportRepository;

public void startSharing(Long userId) {
// 1. 당일 DailyReport 조회
LocalDate today = TodayDateTimeProvider.getTodayDate();
DailyReport report = dailyReportRepository.findByUserIdAndDate(userId, today)
.orElseThrow(() -> new NotFoundException(ErrorCode.DAILY_REPORT_NOT_FOUND));

// 2. 공유 시작
report.startSharing();
}

public void stopSharing(Long userId) {
// 1. 당일 DailyReport 조회
LocalDate today = TodayDateTimeProvider.getTodayDate();
DailyReport report = dailyReportRepository.findByUserIdAndDate(userId, today)
.orElseThrow(() -> new NotFoundException(ErrorCode.DAILY_REPORT_NOT_FOUND));

// 2. 공유 중단
report.stopSharing();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.devkor.ifive.nadab.domain.dailyreport.core.dto;

import com.devkor.ifive.nadab.domain.dailyreport.core.entity.EmotionCode;
import com.devkor.ifive.nadab.domain.user.core.entity.DefaultProfileType;
import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode;

/**
* 피드 조회용 DTO
*/
public record FeedDto(
String nickname,
String profileImageKey,
DefaultProfileType defaultProfileType,
InterestCode interestCode,
String questionText,
String answerContent,
EmotionCode emotionCode
) {
}
Loading