docs: 투표 기능 API 스펙 및 구현 계획 문서 추가#90
Conversation
tlarbals824
left a comment
There was a problem hiding this comment.
코맨트 남겨뒀습니다! 수고하셨어요!
| @PostConstruct | ||
| public void closeExpiredOnStartup() { | ||
| closeExpiredVotes(); | ||
| } |
There was a problem hiding this comment.
너무 좋은 접근이야. 다만 서버 시작할 때, 해당 함수로 인해 http 요청이 block되는지 찾아보면 좋을 것 같아. 만약 그렇다면 비동기로 처리하도록해주면 좋을듯!
| private String thumbnailUrl; | ||
| private String imageUrl; |
| private VoteDuration duration; | ||
| @Enumerated(EnumType.STRING) | ||
| private VoteStatus status; | ||
| private LocalDateTime endAt; |
There was a problem hiding this comment.
인스턴스 또는 컨테이너의 설정에 따라서 시간들이 제각각일 수 있어 보여요. 혹시 Instant(UTC)로 저장하는건 어떻게 생각하시나요?
| @Enumerated(EnumType.STRING) | ||
| private VoteDuration duration; |
There was a problem hiding this comment.
VoteDuration은 뷰 관심사와 가까워보여요! endAt을 외부에서 계산해서 받는다면, 해당 타입은 없어도 괜찮아보이는데 어떻게 생각하시나요?
| if (type == VoteType.IMMERSIVE && imageUrl == null) { | ||
| throw new ImageRequiredException(); | ||
| } |
| public interface VoteCommandUseCase { | ||
| VoteCreateResult create(VoteCreateCommand command); | ||
| ParticipateResult participateAsMember(Long voteId, Long userId, Long optionId); | ||
| ParticipateResult participateAsGuest(Long voteId, String anonymousId, Long optionId); | ||
| void cancel(Long voteId, Long userId); | ||
| } |
| @Override | ||
| public VoteCreateResult create(VoteCreateCommand cmd) { | ||
| // 1. duration 검증 (enum 팩토리에 위임) | ||
| VoteDuration duration = VoteDuration.from(cmd.durationHours()); |
There was a problem hiding this comment.
오히려 외부에서는 enum으로 받고, 내부 커맨드로 넘어올 때에는 시간을 받는게 자연스러워보여요!
왜냐하면 프론트에서 넘겨줄 수 있는 값에 제한이 필요하고 그 제한을 ENUM이 대신해줄 수 있을 것 같아서요! 그리고 내부 도메인은 외부에서 어떤 enum인지 상관 없이 특정 시간동안 유효하다고 값만 받으면 되니까요!
AS-IS
public record VoteCreateCommand(
VoteType type,
String title,
String content,
String thumbnailUrl,
String imageUrl,
int durationHours,
String optionA,
String optionB
) { }
VoteDuration duration = VoteDuration.from(cmd.durationHours());
TO-BE
public record VoteCreateCommand(
VoteType type,
String title,
String content,
String thumbnailUrl,
String imageUrl,
VoteDuration durations,
String optionA,
String optionB
) { }
int duration = command.durations.toDurationValue(); // 러프하게 작성했습니다.
| } | ||
|
|
||
| AiInsight aiInsight = (userId != null && hasParticipated(voteId, userId)) | ||
| ? aiInsightService.generateOrFetch(voteId, userId) |
There was a problem hiding this comment.
ai 인사이트의 경우 언제 생성될까요? 사용자가 조회할때라면 ai 호출로 인해 크게 지연될 수 있어보여요! 만약에 사용자에게 생성중이에요!를 제공할 수 있다면 비동기로 풀어볼 수 있어보여요
There was a problem hiding this comment.
그게 아니라면, 투표가 완료되면 스케쥴러를 통해 생성할수도 있구요!
| - 첫 호출 시 LLM 호출 → vote 테이블의 `ai_insight_headline`, `ai_insight_body` 컬럼에 저장 | ||
| - 이후 호출은 컬럼에서 직접 반환 | ||
| - LLM 호출 실패 시 `aiInsight.available: false`로 응답 (에러 아님) | ||
| - 본인이 만든 youth-policy AI 에이전트 (LangChain4j + Gemini 2.5 Flash) 인프라 재활용 검토 |
| // After | ||
| @RequiredArgsConstructor | ||
| public class ChatCommandService { | ||
| private final VoteQueryUseCase voteQueryUseCase; // ← inbound port 의존 |
| Vote vote = new Vote(); | ||
| vote.type = type; | ||
| vote.title = title; | ||
| vote.content = content; | ||
| vote.thumbnailUrl = thumbnailUrl; | ||
| vote.imageUrl = imageUrl; | ||
| vote.duration = duration; | ||
| vote.status = VoteStatus.ONGOING; | ||
| vote.endAt = LocalDateTime.now().plusHours(duration.getHours()); |
| private Long userId; // 회원이면 set | ||
| private String anonymousId; // 비회원이면 set |
There was a problem hiding this comment.
비회원은 아직 생각은 안해봤는데 가능하다면 하나의 테이블로 관리하는게 더 편해보입니다 결국 회원과 비회원이 모두 같은 기능을 사용해서 분기 없이 참조하는게 더 편해보일것 같습니다
| @PostMapping | ||
| public VoteCreateResponse create( | ||
| @AuthenticationPrincipal Long userId, | ||
| @RequestBody @Valid VoteCreateRequest request) { | ||
| if (userId == null) throw new UnauthorizedException(); | ||
| return VoteCreateResponse.from( | ||
| voteCommandUseCase.create(request.toCommand())); | ||
| } | ||
|
|
||
| @GetMapping("/{voteId}") | ||
| public VoteDetailResponse getDetail( | ||
| @PathVariable Long voteId, | ||
| @AuthenticationPrincipal Long userId, | ||
| @AnonymousId String anonymousId) { ... } | ||
|
|
||
| @PostMapping("/{voteId}/participate") | ||
| public ParticipateResponse participate( | ||
| @PathVariable Long voteId, | ||
| @AuthenticationPrincipal Long userId, | ||
| @AnonymousId String anonymousId, | ||
| @RequestBody ParticipateRequest request) { ... } | ||
|
|
||
| @DeleteMapping("/{voteId}/participate") | ||
| @ResponseStatus(HttpStatus.NO_CONTENT) | ||
| public void cancel( | ||
| @PathVariable Long voteId, | ||
| @AuthenticationPrincipal Long userId) { ... } | ||
| } |
Summary
docs/spec/2026-05-04-vote-api-spec.md: 투표 도메인 설계, 일반형/몰입형/결과 REST API 스펙, 비회원 쿠키 식별 정책, WebSocket 실시간 비율 구조 정의docs/plan/2026-05-04-vote-implementation-plan.md: 헥사고날 아키텍처 + DDD 기반 구현 계획 (패키지 구조, 단계별 구현, 채팅 도메인 mock 흡수 전략, 테스트 전략)문서 구조
Test plan