Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9cc5269
feat(팀 종료 기능 추가(조회에 open 여부 검증 필요)): 팀 종료 기능 추가(조회에 open 여부 검증 필요) DP…
vayaconChoi Sep 9, 2025
b22010f
test
vayaconChoi Sep 11, 2025
f350968
feat(request): update PreferenceRequest DTO example with detailed loc…
projectmiluju Sep 11, 2025
91b7277
refactor(player):프로필 조회 API Swagger 예시 및 문서 location 정보 수정 DP-367
sunsetkk Sep 11, 2025
4f66902
refactor(player):참여 프로젝트/스터디 조회 응답 location 정보 수정 DP-367
sunsetkk Sep 11, 2025
dc41c95
fix(error): 에러 코드 추가
Shin-Yu-1 Sep 11, 2025
0faa097
fix(controller): 채팅 메세지가 없거나 빈 문자열일 경우 에러 반환되도록 추가
Shin-Yu-1 Sep 11, 2025
c47ccdc
Merge branch 'develop' into fix/chat-send-message
Shin-Yu-1 Sep 11, 2025
a10bd4f
fix(project): ProjectRecruitment 연령대 필드 int → Integer 변경 (null 허용) DP…
sunsetkk Sep 11, 2025
8d5697e
fix(project): 프로젝트 모집글 생성/조회 시 선호 연령 무관(null) 처리 통일 DP-367
sunsetkk Sep 11, 2025
e53bcb4
fix(recruitment): 프로젝트/스터디 preferredAges 무관 시 null 통일 DP-367
sunsetkk Sep 11, 2025
0fcbccd
스터디/프로젝트 모집글 연령대 응답 null 처리 통일
sunsetkk Sep 11, 2025
a425bc8
feat(중복 종료 금지 및 에러코드 수정): 중복 종료 금지 및 에러코드 수정 DP-356
vayaconChoi Sep 11, 2025
e5e37e2
feat(에러코드 수정): 에러코드 수정 DP-356
vayaconChoi Sep 11, 2025
c3cb845
Merge pull request #192 from DeepDirect/fix/chat-send-message
projectmiluju Sep 11, 2025
e311cda
Merge pull request #190 from DeepDirect/fix/DP-367-profile-query
projectmiluju Sep 11, 2025
991e4d2
Merge pull request #193 from DeepDirect/fix/DP-367-project-age
projectmiluju Sep 11, 2025
65455a4
Merge pull request #194 from DeepDirect/feature/DP-356-teamEnd
projectmiluju Sep 11, 2025
0fe9ed9
feat(badge): 소셜 로그인도 로그인 뱃지 생성되게 변경 DP-383
projectmiluju Sep 11, 2025
77214b4
feat(study): 스터디 유형으로 필터링 되지 않던 현상 수정 DP-383
projectmiluju Sep 11, 2025
0a995c7
feat(project): 프로젝트/스터디 5개월 이상 필터링 시 5개월만 나오던 현상 수정 DP-383
projectmiluju Sep 11, 2025
9d6274f
fix(service): 정원은 포함이 아닌 일치로 변경 DP-383
projectmiluju Sep 11, 2025
95bc5aa
feat(error): RECRUITMENT_CLOSED 에러 추가 DP-383
projectmiluju Sep 11, 2025
9a05a02
fix(recruitment): 모집 중이 아닌 스터디 및 프로젝트 지원 불가능하게 변경 DP-383
projectmiluju Sep 11, 2025
511d9a8
feat(controller): 팀 관리 API에 대한 태그 추가 DP-383
projectmiluju Sep 12, 2025
f05e91e
fix(repository): 모집글/사용자 기준 승인 신청을 REJECTED로 전환하는 메서드 추가 DP-383
projectmiluju Sep 12, 2025
a423100
fix(repository): 삭제된 항목 포함해 최신 채팅방 멤버를 조회하는 메서드 추가 DP-383
projectmiluju Sep 12, 2025
599c2a5
fix(chat): 채팅방 멤버 추가 로직 보완 및 재신청 처리 DP-383
projectmiluju Sep 12, 2025
4b300da
fix(service): 참가자 정원 검사 로직을 한 명 추가 허용하도록 조정 DP-383
projectmiluju Sep 12, 2025
aea295f
Merge pull request #201 from DeepDirect/fix/DP-383-issue-fix
projectmiluju Sep 12, 2025
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
5 changes: 5 additions & 0 deletions src/main/java/goorm/ddok/chat/controller/ChatController.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

@RestController
Expand Down Expand Up @@ -154,6 +155,10 @@ public ResponseEntity<ApiResponseDto<ChatMessageResponse>> sendMessage(
@Valid @RequestBody ChatMessageRequest request,
Authentication authentication) {

if (!StringUtils.hasText(request.getContentText())) {
throw new GlobalException(ErrorCode.CHAT_MESSAGE_INVALID);
}

String email = authentication.getName();
ChatMessageResponse response = chatMessageService.sendMessage(email, roomId, request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,16 @@ Optional<ChatRoomMember> findFirstByRoom_IdAndDeletedAtIsNullAndUser_IdNotOrderB
Long roomId, Long currentUserId);

boolean existsByRoom_IdAndUser_IdAndDeletedAtIsNull(Long roomId, Long userId);

@Query(value = """
select *
from chat_room_member
where room_id = :roomId
and user_id = :userId
order by id desc
limit 1
""", nativeQuery = true)
Optional<ChatRoomMember> findLatestIncludingDeleted(@Param("roomId") Long roomId,
@Param("userId") Long userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,34 @@ public void createTeamChatRoom(Team team, User leader) {
}

@Transactional
public void addMemberToTeamChat(Team team, User user, TeamMemberRole role) {
ChatRoom room = chatRepository.findByTeam(team)
public void addMemberToTeamChat(Team team, User user, TeamMemberRole teamRole) {
ChatRoom room = chatRepository.findByTeam_Id(team.getId())
.orElseThrow(() -> new GlobalException(ErrorCode.CHAT_ROOM_NOT_FOUND));

boolean already = chatRoomMemberRepository.existsByRoomAndUser(room, user);
if (already) return;
Long roomId = room.getId();
Long userId = user.getId();

ChatMemberRole chatRole = (role == TeamMemberRole.LEADER)
? ChatMemberRole.ADMIN
: ChatMemberRole.MEMBER;
if (chatRoomMemberRepository.existsByRoom_IdAndUser_IdAndDeletedAtIsNull(roomId, userId)) {
return;
}

Optional<ChatRoomMember> any = chatRoomMemberRepository.findLatestIncludingDeleted(roomId, userId);
if (any.isPresent()) {
ChatRoomMember m = any.get();
m.setRoom(room);
m.setUser(user);
m.setRole(ChatMemberRole.MEMBER);
m.restore();
chatRoomMemberRepository.save(m);
return;
}

chatRoomMemberRepository.save(ChatRoomMember.builder()
ChatRoomMember created = ChatRoomMember.builder()
.room(room)
.user(user)
.role(chatRole)
.build());
.role(ChatMemberRole.MEMBER)
.build();
chatRoomMemberRepository.save(created);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface TeamEvaluationRepository extends JpaRepository<TeamEvaluation,

Optional<TeamEvaluation> findTopByTeam_IdOrderByIdDesc(Long teamId);
List<TeamEvaluation> findAllByStatusAndClosesAtBefore(EvaluationStatus status, Instant before);

boolean existsByTeam_IdAndStatus(Long teamId, EvaluationStatus status);
}
3 changes: 3 additions & 0 deletions src/main/java/goorm/ddok/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public enum ErrorCode {
ALREADY_EXPELLED(HttpStatus.BAD_REQUEST, "이미 추방되었거나 탈퇴한 팀원입니다."),
LEADER_CANNOT_BE_EXPELLED(HttpStatus.BAD_REQUEST, "리더는 추방할 수 없습니다."),
LEADER_CANNOT_WITHDRAW(HttpStatus.BAD_REQUEST, "리더는 하차할 수 없습니다."),
CHAT_MESSAGE_INVALID(HttpStatus.BAD_REQUEST, "메세지 내용이 없습니다."),
RECRUITMENT_CLOSED(HttpStatus.BAD_REQUEST, "현재 모집 중이 아닙니다."),


// 401 UNAUTHORIZED
Expand Down Expand Up @@ -121,6 +123,7 @@ public enum ErrorCode {
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "이미 사용 중인 닉네임입니다."),
DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."),
ALREADY_PROCESSED_APPLICATION(HttpStatus.CONFLICT, "이미 처리된 신청입니다."),
ALREADY_CLOSED(HttpStatus.CONFLICT,"이미 종료된 프로젝트/스터디입니다."),

// 429 TOO MANY REQUESTS
KAKAO_RATE_LIMIT(HttpStatus.TOO_MANY_REQUESTS, "카카오 토큰 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요."),
Expand Down
47 changes: 37 additions & 10 deletions src/main/java/goorm/ddok/member/dto/request/PreferenceRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,43 @@
description = "개인화 설정 요청 DTO",
requiredProperties = {"mainPosition", "subPosition", "location", "traits", "birthDate", "activeHours"},
example = """
{
"mainPosition": "백엔드",
"subPosition": ["프론트엔드", "데브옵스"],
"techStacks": ["Java", "Spring Boot", "React"],
"location": { "latitude": 37.5665, "longitude": 126.9780, "address": "서울특별시 강남구 테헤란로 123" },
"traits": ["협업", "문제 해결"],
"birthDate": "1997-10-10",
"activeHours": { "start": "19", "end": "23" }
}
"""
{
"mainPosition": "백엔드",
"subPosition": [
"풀스택",
"데브옵스"
],
"techStacks": [
"Java",
"Spring Boot",
"MySQL",
"Docker",
"AWS"
],
"location": {
"address": "전북 익산시 망산길 11-17",
"region1depthName": "전북",
"region2depthName": "익산시",
"region3depthName": "부송동",
"roadName": "망산길",
"mainBuildingNo": "11",
"subBuildingNo": "17",
"zoneNo": "54547",
"latitude": 35.976749396987046,
"longitude": 126.99599512792346
},
"traits": [
"협업",
"문제 해결",
"학습 능력"
],
"birthDate": "1995-03-15",
"activeHours": {
"start": "09",
"end": "18"
}
}
"""
)
public class PreferenceRequest {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package goorm.ddok.member.service;

import goorm.ddok.badge.service.BadgeService;
import goorm.ddok.global.security.jwt.JwtTokenProvider;
import goorm.ddok.global.security.token.RefreshTokenService;
import goorm.ddok.member.domain.User;
Expand All @@ -20,6 +21,7 @@ public class SocialSignInService {

private final KakaoOAuthService kakaoOAuthService;
private final SocialAuthService socialAuthService;
private final BadgeService badgeService;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;

Expand All @@ -32,6 +34,7 @@ public SignInResponse signInWithKakaoCode(String authorizationCode,
KakaoOAuthService.KakaoTokenResponse tokenRes =
kakaoOAuthService.exchangeCodeForAccessToken(authorizationCode, redirectUri);


String kakaoAccessToken = tokenRes.getAccess_token();
String kakaoRefreshToken = tokenRes.getRefresh_token(); // 카카오 refresh_token (필요 시 저장/재사용)

Expand All @@ -43,6 +46,8 @@ public SignInResponse signInWithKakaoCode(String authorizationCode,
kuser.kakaoId(), kuser.email(), kuser.nickname(), kuser.profileImageUrl()
);

badgeService.grantLoginBadge(user);

// 4) JWT 발급
String accessToken = jwtTokenProvider.createToken(user.getId());
String refreshToken = jwtTokenProvider.createRefreshToken(user.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ public class ProfileProjectQueryController {
"title": "구지라지 프로젝트",
"teamStatus": "CLOSED",
"location": {
"latitude": 37.5665,
"longitude": 126.9780,
"address": "서울특별시 강남구 테헤란로…"
"address": "전북 익산시",
"region1depthName": "전북",
"region2depthName": "익산시",
"region3depthName": "부송동",
"roadName": "망산길",
"mainBuildingNo": "11",
"subBuildingNo": "17",
"zoneNo": "54547",
"latitude": 35.976749396987046,
"longitude": 126.99599512792346
},
"period": {
"start": "2025-08-01",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class ProfileStudyQueryController {
"title": "면접 스터디",
"teamStatus": "CLOSED",
"location": {
"address": "전북 익산시 부송동 망산길 11-17",
"address": "전북 익산시",
"region1depthName": "전북",
"region2depthName": "익산시",
"region3depthName": "부송동",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,56 @@
description = "프로젝트 위치 DTO",
example = """
{
"latitude": 37.5665,
"longitude": 126.9780,
"address": "서울특별시 강남구 테헤란로…"
"address": "전북 익산시",
"region1depthName": "전북",
"region2depthName": "익산시",
"region3depthName": "부송동",
"roadName": "망산길",
"mainBuildingNo": "11",
"subBuildingNo": "17",
"zoneNo": "54547",
"latitude": 35.976749396987046,
"longitude": 126.99599512792346
}
"""
)
public class ProjectLocationResponse {
private String address;
private String region1depthName;
private String region2depthName;
private String region3depthName;
private String roadName;
private String mainBuildingNo;
private String subBuildingNo;
private String zoneNo;
private BigDecimal latitude;
private BigDecimal longitude;
private String address;

public static ProjectLocationResponse from(ProjectRecruitment project) {
String address = String.join(" ",
project.getRegion1depthName() != null ? project.getRegion1depthName() : "",
project.getRegion2depthName() != null ? project.getRegion2depthName() : "",
project.getRegion3depthName() != null ? project.getRegion3depthName() : "",
project.getRoadName() != null ? project.getRoadName() : ""
).trim();
String region1 = project.getRegion1depthName() != null ? project.getRegion1depthName() : "";
String region2 = project.getRegion2depthName() != null ? project.getRegion2depthName() : "";
String address = normalizeRegion1(region1) + " " + region2;

return ProjectLocationResponse.builder()
.address(address)
.region1depthName(project.getRegion1depthName())
.region2depthName(project.getRegion2depthName())
.region3depthName(project.getRegion3depthName())
.roadName(project.getRoadName())
.mainBuildingNo(project.getMainBuildingNo())
.subBuildingNo(project.getSubBuildingNo())
.zoneNo(project.getZoneNo())
.latitude(project.getLatitude())
.longitude(project.getLongitude())
.address(address.isEmpty() ? null : address)
.build();
}

private static String normalizeRegion1(String region1) {
return region1.replace("특별시", "")
.replace("광역시", "")
.replace("자치시", "")
.replace("도", "")
.trim();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
description = "스터디 위치 DTO (LocationDto 통일)",
example = """
{
"address": "전북 익산시 부송동 망산길 11-17",
"address": "전북 익산시",
"region1depthName": "전북",
"region2depthName": "익산시",
"region3depthName": "부송동",
Expand All @@ -45,22 +45,12 @@ public class StudyLocationResponse {
private BigDecimal longitude;

public static StudyLocationResponse from(StudyRecruitment study) {
// 전체 주소 문자열 합성
StringBuilder sb = new StringBuilder();
if (study.getRegion1depthName() != null) sb.append(study.getRegion1depthName()).append(" ");
if (study.getRegion2depthName() != null) sb.append(study.getRegion2depthName()).append(" ");
if (study.getRegion3depthName() != null) sb.append(study.getRegion3depthName()).append(" ");
if (study.getRoadName() != null) sb.append(study.getRoadName()).append(" ");
if (study.getMainBuildingNo() != null) {
sb.append(study.getMainBuildingNo());
if (study.getSubBuildingNo() != null && !study.getSubBuildingNo().isBlank()) {
sb.append("-").append(study.getSubBuildingNo());
}
}
String fullAddress = sb.toString().trim().replaceAll("\\s+", " ");
String region1 = study.getRegion1depthName() != null ? study.getRegion1depthName() : "";
String region2 = study.getRegion2depthName() != null ? study.getRegion2depthName() : "";
String address = normalizeRegion1(region1) + " " + region2;

return StudyLocationResponse.builder()
.address(fullAddress.isEmpty() ? null : fullAddress)
.address(address)
.region1depthName(study.getRegion1depthName())
.region2depthName(study.getRegion2depthName())
.region3depthName(study.getRegion3depthName())
Expand All @@ -72,4 +62,12 @@ public static StudyLocationResponse from(StudyRecruitment study) {
.longitude(study.getLongitude())
.build();
}

private static String normalizeRegion1(String region1) {
return region1.replace("특별시", "")
.replace("광역시", "")
.replace("자치시", "")
.replace("도", "")
.trim();
}
}
10 changes: 4 additions & 6 deletions src/main/java/goorm/ddok/project/domain/ProjectRecruitment.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,10 @@ public class ProjectRecruitment {
@Column(precision = 9, scale = 6)
private BigDecimal longitude;

/** 최소 연령 (무관이면 0) */
@Column(nullable = false)
private int ageMin;
/** 최대 연령 (무관이면 0) */
@Column(nullable = false)
private int ageMax;
/** 최소 연령 */
private Integer ageMin;
/** 최대 연령 */
private Integer ageMax;

/** 모집 정원 (1-7명) */
@Column(nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ select count(a)

boolean existsByUser_IdAndPosition_ProjectRecruitment_IdAndStatus(
Long userId, Long projectId, ApplicationStatus status);


@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
update ProjectApplication a
set a.status = :rejected
where a.position.projectRecruitment.id = :recruitmentId
and a.user.id = :userId
and a.status = :approved
""")
int markApprovedAsRejectedByRecruitmentAndUser(@Param("recruitmentId") Long recruitmentId,
@Param("userId") Long userId,
@Param("approved") goorm.ddok.project.domain.ApplicationStatus approved,
@Param("rejected") goorm.ddok.project.domain.ApplicationStatus rejected);

default int markApprovedAsRejectedByRecruitmentAndUser(Long recruitmentId, Long userId) {
return markApprovedAsRejectedByRecruitmentAndUser(
recruitmentId, userId,
goorm.ddok.project.domain.ApplicationStatus.APPROVED,
goorm.ddok.project.domain.ApplicationStatus.REJECTED
);
}
}


Loading
Loading