Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public enum ErrorStatus implements BaseErrorCode {
// Badge 관련 에러
BADGE_NOT_FOUND(HttpStatus.NOT_FOUND, "BADGE4004", "해당 BADGE를 찾을 수 없습니다."),
BADGE_INSUFFICIENT_TOP_MEMBERS(HttpStatus.BAD_REQUEST, "BADGE4001", "팀원이 1명 이하라 뱃지 수여 조건을 만족하지 않습니다. 팀원은 최소 2명 이상이어야 합니다."),
BADGE_INSUFFICIENT_TODO_COUNTS(HttpStatus.BAD_REQUEST, "BADGE4002", "TODO를 완료한 사람이 존재하지 않습니다."),
BADGE_INSUFFICIENT_TODO_COUNTS(HttpStatus.BAD_REQUEST, "BADGE4002", "TODO를 완료한 사람이 2명 미만입니다. 최소 2명 이상이어야 합니다."),


// 검색 관련 에러
INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, "SEARCH_STATUS4000", "검색 Type 형식이 올바르지 않습니다 (pot/feed)"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public ResponseEntity<ApiResponse<List<PotBadgeMemberDto>>> getBadgeMembersByPot
@Operation(summary = "팟에서 가장 많은 `투두를 완료한' 멤버에게 '할 일 정복자' 뱃지 부여")
@PostMapping("/{potId}")
@ApiErrorCodeExamples({
ErrorStatus.BADGE_NOT_FOUND,
ErrorStatus.BADGE_INSUFFICIENT_TODO_COUNTS,
ErrorStatus.POT_MEMBER_NOT_FOUND
})
Expand Down
38 changes: 21 additions & 17 deletions src/main/java/stackpot/stackpot/badge/service/BadgeServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,36 @@ public Badge getBadge(Long badgeId) {
@Transactional
@Override
public void assignBadgeToTopMembers(Long potId) {
// 1. 완료된 Todo 개수가 0이면 예외
long completedTodoCount = userTodoRepository.countByPot_PotIdAndStatus(potId, TodoStatus.COMPLETED);
if (completedTodoCount == 0) {
// case [2] 총 팀 멤버가 2명 이하 -> 배지 부여 X (아무 동작 안 함)
long memberCount = potMemberRepository.countByPot_PotId(potId);
if (memberCount <= 2) return;

// 완료한 '서로 다른 사용자 수' 집계
long completedUserCount =
userTodoRepository.countDistinctUserIdsByPotAndStatus(potId, TodoStatus.COMPLETED);

// case [3] 팀 멤버 2명 이상 && 완료 사용자 수 < 2 -> 에러
if (completedUserCount < 2) {
throw new PotHandler(ErrorStatus.BADGE_INSUFFICIENT_TODO_COUNTS);
}

// 2. Todo를 많이 완료한 상위 2명의 userId 조회
// case [1] 팀 멤버 2명 이상 && 완료 사용자 수 >= 2 -> 정상 (상위 2명 배지 부여)
List<Long> topUserIds = userTodoRepository.findTop2UserIds(potId, TodoStatus.COMPLETED);
if (topUserIds.size() < 2) {
throw new PotHandler(ErrorStatus.BADGE_INSUFFICIENT_TOP_MEMBERS);
}

// 3. PotMember 조회
List<PotMember> topPotMembers = topUserIds.stream()
.map(userId -> potMemberRepository.findByPot_PotIdAndUser_Id(potId, userId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_MEMBER_NOT_FOUND)))
.toList();

// 4. Todo 배지 부여
Badge badge = getBadge(1L);
for (PotMember potMember : topPotMembers) {
PotMemberBadge potMemberBadge = PotMemberBadge.builder()
.badge(badge)
.potMember(potMember)
.build();
potMemberBadgeRepository.save(potMemberBadge);
for (Long userId : topUserIds) {
PotMember pm = potMemberRepository.findByPot_PotIdAndUser_Id(potId, userId)
.orElseThrow(() -> new PotHandler(ErrorStatus.POT_MEMBER_NOT_FOUND));

potMemberBadgeRepository.save(
PotMemberBadge.builder()
.badge(badge)
.potMember(pm)
.build()
);
}
}

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/stackpot/stackpot/common/util/DateFormatter.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package stackpot.stackpot.common.util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class DateFormatter {
private DateFormatter() {}

private static final DateTimeFormatter INPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd");
public static String dotFormatter(java.time.LocalDate date) {
return (date != null) ? date.format(DATE_FORMATTER) : "N/A";
Expand All @@ -15,4 +19,29 @@ public static String koreanFormatter(java.time.LocalDateTime date) {
return (date != null) ? date.format(DATE_TIME_FORMATTER) : "N/A";
}



public static String format(String dateStr) {
if (dateStr == null || dateStr.isBlank()) return null;
try {
LocalDate date = LocalDate.parse(dateStr, INPUT_FORMATTER);
return date.format(DATE_FORMATTER);
} catch (DateTimeParseException e) {
return dateStr;
}
}

// yyyy.MM 형식 변환
public static String formatToMonth(String dateStr) {
if (dateStr == null || dateStr.isBlank()) return null;
try {
LocalDate date = LocalDate.parse(dateStr, INPUT_FORMATTER);
return date.format(OUTPUT_FORMATTER);
} catch (DateTimeParseException e) {
return dateStr;
}
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import stackpot.stackpot.apiPayload.ApiResponse;
import stackpot.stackpot.badge.dto.CompletedPotBadgeResponseDto;
import stackpot.stackpot.pot.dto.AppealContentDto;
import stackpot.stackpot.pot.dto.OngoingPotResponseDto;
import stackpot.stackpot.pot.dto.PotNameUpdateRequestDto;
import stackpot.stackpot.pot.service.pot.MyPotService;
import stackpot.stackpot.pot.service.pot.PotCommandService;
import java.util.List;
Expand Down Expand Up @@ -80,4 +82,15 @@ public ResponseEntity<ApiResponse<String>> patchDelegate(@PathVariable("pot_id")
String responseMessage = myPotService.patchDelegate(potId, memberId);
return ResponseEntity.ok(ApiResponse.onSuccess(responseMessage));
}

@Operation(summary = "팟 이름 수정 API")
@PatchMapping("/{pot_id}/rename")
public ResponseEntity<ApiResponse<String>> updatePotName(
@PathVariable Long pot_id,
@Valid @RequestBody PotNameUpdateRequestDto request
) {
String res = potCommandService.updatePotName(pot_id, request);
return ResponseEntity.ok(ApiResponse.onSuccess(res));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class PotController {

@Operation(summary = "팟 생성 API", description = """
- potStatus: RECRUITING / ONGOING / COMPLETED
- potStartDate, potEndDate: yyyy.MM 형식 (예: 2025.08)
- potModeOfOperation: ONLINE / OFFLINE / HYBRID
- Role: FRONTEND / BACKEND / DESIGN / PLANNING
""")
Expand Down Expand Up @@ -152,4 +153,5 @@ public ResponseEntity<ApiResponse<Map<String, Object>>> getMyRecruitingPots(
return ResponseEntity.ok(ApiResponse.onSuccess(response));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class PotMemberController {
private final PotMemberCommandService potMemberCommandService;
private final PotMemberQueryService potMemberQueryService;

@Operation(summary = "팟 멤버 정보 (KAKAOID, 닉네임) 조회 API")
@Operation(summary = "팟 멤버 정보 조회 API")
@GetMapping
public ResponseEntity<ApiResponse<List<PotMemberInfoResponseDto>>> getPotMembers(
@PathVariable("pot_id") Long potId) {
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/stackpot/stackpot/pot/converter/PotConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public class PotConverter{
public Pot toEntity(PotRequestDto requestDto, User user) {
return Pot.builder()
.potName(requestDto.getPotName())
.potStartDate(requestDto.getPotStartDate())
.potEndDate(requestDto.getPotEndDate())
.potStartDate(String.valueOf(requestDto.getPotStartDate()))
.potEndDate(String.valueOf(requestDto.getPotEndDate()))
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

String.valueOf(...)는 null을 "null" 문자열로 저장 — 데이터 오염 가능성

requestDto의 값이 null일 때 "null"이라는 문자열이 엔티티에 저장됩니다. 이는 후속 포맷팅/조회 시 예기치 않은 동작을 야기합니다.

아래처럼 null-안전하게 직접 할당하세요(요청 DTO 타입이 String이라면 단순 대입, YearMonth/LocalDate라면 명시적 포맷터로 변환).

-                .potStartDate(String.valueOf(requestDto.getPotStartDate()))
-                .potEndDate(String.valueOf(requestDto.getPotEndDate()))
+                .potStartDate(requestDto.getPotStartDate())
+                .potEndDate(requestDto.getPotEndDate())

추가 권장:

  • 이미 "null" 문자열이 저장된 레코드가 있을 수 있으니, 마이그레이션으로 해당 값을 실제 NULL로 정정하는 작업도 고려하세요.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.potStartDate(String.valueOf(requestDto.getPotStartDate()))
.potEndDate(String.valueOf(requestDto.getPotEndDate()))
.potStartDate(requestDto.getPotStartDate())
.potEndDate(requestDto.getPotEndDate())
🤖 Prompt for AI Agents
In src/main/java/stackpot/stackpot/pot/converter/PotConverter.java around lines
33–34, using String.valueOf(requestDto.getPotStartDate()) and
String.valueOf(requestDto.getPotEndDate()) will store the literal "null" when
the DTO values are null; change these assignments to be null-safe: if the DTO
fields are already String assign them directly (requestDto.getPotStartDate()),
otherwise if they are YearMonth/LocalDate convert using an explicit
DateTimeFormatter and handle null by keeping the target field null (e.g., field
= dto == null ? null : formatter.format(dto)); also consider adding a data
migration to replace any existing "null" string values in the database with
actual NULLs.

.potRecruitmentDeadline(requestDto.getPotRecruitmentDeadline())
.potLan(requestDto.getPotLan())
.potContent(requestDto.getPotContent())
Expand All @@ -45,8 +45,8 @@ public PotResponseDto toDto(Pot entity, List<PotRecruitmentDetails> recruitmentD
return PotResponseDto.builder()
.potId(entity.getPotId())
.potName(entity.getPotName())
.potStartDate(entity.getPotStartDate())
.potEndDate(entity.getPotEndDate())
.potStartDate(DateFormatter.format(entity.getPotStartDate()))
.potEndDate(DateFormatter.format(entity.getPotEndDate()))
.potRecruitmentDeadline(entity.getPotRecruitmentDeadline())
.potLan(entity.getPotLan())
.potContent(entity.getPotContent())
Expand Down Expand Up @@ -94,8 +94,8 @@ public CompletedPotResponseDto toCompletedPotResponseDto(Pot pot, String formatt
return CompletedPotResponseDto.builder()
.potId(pot.getPotId())
.potName(pot.getPotName())
.potStartDate(pot.getPotStartDate())
.potEndDate(pot.getPotEndDate())
.potStartDate(DateFormatter.format(pot.getPotStartDate()))
.potEndDate(DateFormatter.format(pot.getPotEndDate()))
.potRecruitmentDeadline(pot.getPotRecruitmentDeadline())
.potLan(pot.getPotLan())
.members(formattedMembers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public PotDetailResponseDto toPotDetailResponseDto(User user, Pot pot, String re
.isOwner(isOwner)
.potId(pot.getPotId())
.potName(pot.getPotName())
.potStartDate(pot.getPotStartDate())
.potEndDate(pot.getPotEndDate())
.potStartDate(DateFormatter.formatToMonth(pot.getPotStartDate()))
.potEndDate(DateFormatter.formatToMonth(pot.getPotEndDate()))
.potRecruitmentDeadline(pot.getPotRecruitmentDeadline())
.potLan(pot.getPotLan())
.potStatus(pot.getPotStatus())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
import org.springframework.stereotype.Component;
import stackpot.stackpot.user.entity.enums.Role;


import java.util.Map;

@Component
public class PotMemberConverter{

Expand All @@ -26,16 +23,6 @@ public PotMember toEntity(User user, Pot pot, PotApplication application, Boolea
.appealContent(null)
.build();
}
public PotMember toCreatorEntity(User user, Pot pot, Role role) {
return PotMember.builder()
.user(user)
.pot(pot)
.potApplication(null)
.roleName(role)
.owner(true)
.appealContent(null)
.build();
}

public PotMemberAppealResponseDto toDto(PotMember entity) {
String roleName = entity.getRoleName() != null ? entity.getRoleName().name() : "멤버";
Expand All @@ -57,7 +44,6 @@ public PotMemberAppealResponseDto toDto(PotMember entity) {
.roleName(roleName)
.nickname(nicknameWithRole)
.appealContent(entity.getAppealContent())
.kakaoId(entity.getUser().getKakaoId())
.build();
}

Expand All @@ -69,7 +55,6 @@ public PotMemberInfoResponseDto toKaKaoCreatorDto(PotMember entity) {
.potMemberId(entity.getPotMemberId())
.nickname(nicknameWithRole)
.potRole(entity.getRoleName().name())
.kakaoId(null)
.owner(true)
.build();
}
Expand All @@ -93,7 +78,6 @@ public PotMemberInfoResponseDto toKaKaoMemberDto(PotMember entity) {
return PotMemberInfoResponseDto.builder()
.potMemberId(entity.getPotMemberId())
.nickname(nicknameWithRole)
.kakaoId(entity.getUser().getKakaoId())
.owner(false)
.potRole(roleName)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class CompletedPotRequestDto {
@NotBlank(message = "팟 이름은 필수입니다.")
private String potName;

private LocalDate potStartDate;
private String potStartDate;

@NotBlank(message = "사용 언어는 필수입니다.")
private String potLan;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
public class PotMemberInfoResponseDto {
private Long potMemberId;
private String nickname; // 닉네임 + 역할
private String kakaoId;
private String potRole;
private boolean owner;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package stackpot.stackpot.pot.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PotNameUpdateRequestDto {
private String potName;
}
14 changes: 11 additions & 3 deletions src/main/java/stackpot/stackpot/pot/dto/PotRequestDto.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package stackpot.stackpot.pot.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
Expand All @@ -16,27 +17,34 @@
public class PotRequestDto {

@NotBlank(message = "팟 이름은 필수입니다.")
@Schema(description = "팟 이름", example = "스터디용 백엔드 프로젝트")
private String potName;

@Schema(description = "팟 시작 날짜 (yyyy.MM)", example = "2025.08")
private String potStartDate;

@Schema(description = "팟 종료 날짜 (yyyy.MM)", example = "2025.10")
private String potEndDate;

@Schema(description = "모집 마감일 (yyyy-MM-dd)", example = "2025-08-18")
private LocalDate potRecruitmentDeadline;

@Schema(description = "팟 역할", example = "BACKEND")
private Role potRole;

@NotBlank(message = "사용 언어는 필수입니다.")
@Schema(description = "사용 언어", example = "Java")
private String potLan;

@Schema(description = "팟 설명", example = "이 프로젝트는 REST API 서버를 구현하는 것을 목표로 합니다.")
private String potContent;

// @NotBlank(message = "팟 상태는 필수입니다.")
// private String potStatus;

@Schema(description = "운영 방식", example = "ONLINE")
private String potModeOfOperation;

@Schema(description = "팟 요약", example = "백엔드 중심의 API 개발 스터디 프로젝트")
private String potSummary;

@Schema(description = "모집 역할 리스트")
private List<PotRecruitmentRequestDto> recruitmentDetails;
}
1 change: 1 addition & 0 deletions src/main/java/stackpot/stackpot/pot/entity/Pot.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class Pot extends BaseEntity {
@OneToMany(mappedBy = "pot", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<PotMember> potMembers;

@Setter
@Column(nullable = false, length = 255)
private String potName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ public interface PotMemberRepository extends JpaRepository<PotMember, Long> {
@Query("DELETE FROM PotMember pm WHERE pm.pot.potId = :potId")
void deleteByPotId(@Param("potId") Long potId);

Optional<PotMember> findByPot_PotIdAndUser_Id(Long potId, Long userId);

@Query("SELECT pm FROM PotMember pm WHERE pm.pot.potId = :potId AND pm.user.id = :userId")
PotMember findByPotIdAndUserId(Long potId, Long userId);

Expand Down Expand Up @@ -94,4 +92,8 @@ public interface PotMemberRepository extends JpaRepository<PotMember, Long> {
@Modifying
@Query("DELETE FROM PotMember pm WHERE pm.pot.potId IN :potIds AND pm.user.id = :userId")
void deleteByUserIdAndPotIdIn(@Param("userId") Long userId, @Param("potIds") List<Long> potIds);

long countByPot_PotId(Long potId);
Optional<PotMember> findByPot_PotIdAndUser_Id(Long potId, Long userId);

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package stackpot.stackpot.pot.service.pot;

import stackpot.stackpot.pot.dto.CompletedPotRequestDto;
import stackpot.stackpot.pot.dto.PotNameUpdateRequestDto;
import stackpot.stackpot.pot.dto.PotRequestDto;
import stackpot.stackpot.pot.dto.PotResponseDto;

Expand All @@ -19,4 +20,6 @@ public interface PotCommandService {
void patchLikes(Long potId, Long applicationId, Boolean liked);

PotResponseDto updateCompletedPot(Long potId, CompletedPotRequestDto requestDto);

String updatePotName(Long potId, PotNameUpdateRequestDto request);
}
Loading