-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#27 조직 멤버 초대 및 수락 구현 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
dd971ed
99215bb
8e49ab3
8d0eb8d
2835466
61a343c
f2008fb
de41291
2c48429
efc79ee
08144a9
7a50428
0a26605
1892a75
77408be
cad16a9
474ad2d
9e5bdf9
1a66823
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,46 +11,52 @@ | |||||||||
| import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgMemberRepository; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgRepository; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User; | ||||||||||
| import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository; | ||||||||||
| import com.whereyouad.WhereYouAd.global.utils.RedisUtil; | ||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||
| import org.springframework.stereotype.Service; | ||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||
|
|
||||||||||
| import java.util.List; | ||||||||||
| import java.util.UUID; | ||||||||||
|
|
||||||||||
| @Service | ||||||||||
| @Transactional | ||||||||||
| @RequiredArgsConstructor | ||||||||||
| public class OrgServiceImpl implements OrgService{ | ||||||||||
| public class OrgServiceImpl implements OrgService { | ||||||||||
|
|
||||||||||
| private final OrgRepository orgRepository; | ||||||||||
| private final OrgMemberRepository orgMemberRepository; | ||||||||||
| private final UserRepository userRepository; | ||||||||||
|
|
||||||||||
| //조직(워크스페이스) 생성 메서드 | ||||||||||
| private final RedisUtil redisUtil; | ||||||||||
| private final EmailService emailService; | ||||||||||
|
|
||||||||||
| // 조직(워크스페이스) 생성 메서드 | ||||||||||
| public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request) { | ||||||||||
|
|
||||||||||
| //유저 정보 추출 | ||||||||||
| // 유저 정보 추출 | ||||||||||
| User user = userRepository.findById(userId) | ||||||||||
| .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| //만약 해당 User 가 이미 같은 name 을 가진 Organization 에 속해있으면 예외처리 | ||||||||||
| //해당 User 의 OrgMember 를 모두 추출해서, | ||||||||||
| // 만약 해당 User 가 이미 같은 name 을 가진 Organization 에 속해있으면 예외처리 | ||||||||||
| // 해당 User 의 OrgMember 를 모두 추출해서, | ||||||||||
| List<OrgMember> orgMemberByUser = orgMemberRepository.findOrgMemberByUser(user); | ||||||||||
| for (OrgMember orgMember : orgMemberByUser) { | ||||||||||
| //OrgMember 내부 Organization 의 name 이 생성하려는 request 의 name 과 같으면 | ||||||||||
| // OrgMember 내부 Organization 의 name 이 생성하려는 request 의 name 과 같으면 | ||||||||||
| if (orgMember.getOrganization().getName().equals(request.name())) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_NAME_DUPLICATE); //예외처리 | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_NAME_DUPLICATE); // 예외처리 | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 생성 | ||||||||||
| // 조직 생성 | ||||||||||
| Organization organization = OrgConverter.toOrganization(userId, request); | ||||||||||
|
|
||||||||||
| //OrgMember 생성 | ||||||||||
| // OrgMember 생성 | ||||||||||
| OrgMember orgMember = OrgMemberConverter.toOrgMemberADMIN(user, organization); | ||||||||||
|
|
||||||||||
| orgRepository.save(organization); | ||||||||||
|
|
@@ -60,76 +66,139 @@ public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create requ | |||||||||
| } | ||||||||||
|
|
||||||||||
| public OrgResponse.Read getOrganization(Long userId) { | ||||||||||
| //TODO | ||||||||||
| // TODO | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 정보 수정 메서드 | ||||||||||
| // 조직 정보 수정 메서드 | ||||||||||
| public OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request) { | ||||||||||
| Organization organization = orgRepository.findById(orgId) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| //만약 조직 정보 수정을 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| // 만약 조직 정보 수정을 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| if (!organization.getOwnerUserId().equals(userId)) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 정보 수정 | ||||||||||
| // 조직 정보 수정 | ||||||||||
| organization.modifyInfo(request); | ||||||||||
|
|
||||||||||
| //변환 된 필드값과 해당 조직의 Id, updatedAt 가 포함된 DTO 로 반환 | ||||||||||
| // 변환 된 필드값과 해당 조직의 Id, updatedAt 가 포함된 DTO 로 반환 | ||||||||||
| return OrgConverter.toUpdatedResponse(organization); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public OrgResponse.Delete restoreOrganization(Long userId, Long orgId) { | ||||||||||
| Organization organization = orgRepository.findById(orgId) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| //만약 조직 복구 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| // 만약 조직 복구 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| if (!organization.getOwnerUserId().equals(userId)) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직이 이미 활성화 상태라면, | ||||||||||
| // 조직이 이미 활성화 상태라면, | ||||||||||
| if (organization.getStatus() == OrgStatus.ACTIVE) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_ALREADY_ACTIVE); //예외처리 | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_ALREADY_ACTIVE); // 예외처리 | ||||||||||
| } | ||||||||||
|
|
||||||||||
| organization.restoreDelete(); //조직 Soft Delete 복구 | ||||||||||
| organization.restoreDelete(); // 조직 Soft Delete 복구 | ||||||||||
|
|
||||||||||
| return OrgConverter.toRestoredResponse(organization); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 삭제 메서드 -> Hard Delete (DB 에서 완전히 제거) | ||||||||||
| // 조직 삭제 메서드 -> Hard Delete (DB 에서 완전히 제거) | ||||||||||
| public void removeOrganization(Long userId, Long orgId) { | ||||||||||
| Organization organization = orgRepository.findById(orgId) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| //만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| // 만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| if (!organization.getOwnerUserId().equals(userId)) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //해당 조직에 가입된 모든 회원들의 가입 정보 삭제 | ||||||||||
| // 해당 조직에 가입된 모든 회원들의 가입 정보 삭제 | ||||||||||
| List<OrgMember> orgMembers = orgMemberRepository.findOrgMemberByOrg(organization); | ||||||||||
|
|
||||||||||
| orgMemberRepository.deleteAll(orgMembers); | ||||||||||
|
|
||||||||||
| //조직 실제 삭제 | ||||||||||
| // 조직 실제 삭제 | ||||||||||
| orgRepository.delete(organization); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 삭제 메서드 -> Soft Delete (status 만 DELETED 로 변경) | ||||||||||
| // 조직 삭제 메서드 -> Soft Delete (status 만 DELETED 로 변경) | ||||||||||
| public void removeOrganizationSoft(Long userId, Long orgId) { | ||||||||||
| Organization organization = orgRepository.findById(orgId) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| //만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| // 만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, | ||||||||||
| if (!organization.getOwnerUserId().equals(userId)) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| //조직 status 만 DELETED 로 변경 후 종료 | ||||||||||
| // 조직 status 만 DELETED 로 변경 후 종료 | ||||||||||
| organization.softDelete(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| @Override | ||||||||||
| // 조직 초대 이메일 보내기 | ||||||||||
| public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email) { | ||||||||||
| Organization organization = orgRepository.findById(orgId) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| // 초대 완료 여부 확인, 가입 여부에 상관 없이 이메일 발송 | ||||||||||
| userRepository.findUserByEmail(email).ifPresent(user -> { | ||||||||||
| if (orgMemberRepository.existsByUser(user)) | ||||||||||
| new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); | ||||||||||
| }); | ||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| // Redis key = 임의의 UUID 토큰(조직 초대 이메일 내 링크를 구별) | ||||||||||
| String token = UUID.randomUUID().toString(); | ||||||||||
| // Redis value = 조직 아이디와 이메일의 조합 | ||||||||||
| String value = orgId + ":" + email; | ||||||||||
| redisUtil.setDataExpire("INVITE:" + token, value, 3600 * 24L); | ||||||||||
|
|
||||||||||
| emailService.sendEmailForOrgInvitation(token, email, organization.getName()); | ||||||||||
|
|
||||||||||
| return new OrgResponse.OrgInvitationResponse(orgId, "조직 멤버 초대 이메일을 전송하였습니다.", email); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| @Override | ||||||||||
| // 조직 초대 수락 (이메일 내 링크 클릭 시) | ||||||||||
| public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token) { | ||||||||||
| // 링크 만료 또는 유효하지 않을 시 | ||||||||||
| if (token == null) | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); | ||||||||||
|
|
||||||||||
| // Redis 내 UUID(key)에 대한 email(value) 비교 | ||||||||||
| String value = redisUtil.getData("INVITE:" + token); | ||||||||||
| if (value == null) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| String[] valueForSplit = value.split(":"); | ||||||||||
| String email = valueForSplit[1]; | ||||||||||
|
Comment on lines
+246
to
+247
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find the file and check its structure
find . -type f -name "OrgServiceImpl.java" | head -5Repository: WhereYouAd/WhereYouAd-Backend Length of output: 170 🏁 Script executed: #!/bin/bash
# Once we have the file path, let's examine the relevant sections
# Find lines 200-270 to see the full context of both methods
if [ -f "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" ]; then
sed -n '200,270p' "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java"
else
find . -name "OrgServiceImpl.java" -exec sed -n '200,270p' {} \;
fiRepository: WhereYouAd/WhereYouAd-Backend Length of output: 3050 🏁 Script executed: #!/bin/bash
# Let's also check where the Redis value is being set (the "orgId:email" format)
if [ -f "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" ]; then
grep -n "redisTemplate\|setValue\|set(" "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" | head -20
fiRepository: WhereYouAd/WhereYouAd-Backend Length of output: 55 🏁 Script executed: #!/bin/bash
# Search for Redis operations and INVITE token handling
rg -n "INVITE|redis|set\(" --type=java "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java"Repository: WhereYouAd/WhereYouAd-Backend Length of output: 307
Redis에 예: 더 근본적으로, Redis 값 포맷에 콜론이 추가되거나 변경되는 순간 이 파싱 로직은 깨질 수 있습니다. ✅ 수정 제안- String[] valueForSplit = value.split(":");
+ String[] valueForSplit = value.split(":", 2);
String email = valueForSplit[1];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| // 로그인한 사용자 검증 | ||||||||||
| User user = userRepository.findById(userId) | ||||||||||
| .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| // 초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하는지 확인 | ||||||||||
| if (!user.getEmail().equals(email)) { | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_INVITATION_FORBIDDEN_USER); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Organization organization = orgRepository.findById(Long.parseLong(valueForSplit[0])) | ||||||||||
| .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); | ||||||||||
|
|
||||||||||
| // 이미 멤버인지 중복 체크 | ||||||||||
| if (orgMemberRepository.existsByUser(user)) | ||||||||||
| throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); | ||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); | ||||||||||
jinnieusLab marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| // Redis 사용 토큰 삭제 | ||||||||||
| redisUtil.deleteData("INVITE:" + token); | ||||||||||
|
|
||||||||||
| return new OrgResponse.OrgInvitationResponse(organization.getId(), "조직 멤버 초대 이메일을 수락하였습니다.", email); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,18 +8,28 @@ | |
| @Getter | ||
| @AllArgsConstructor | ||
| public enum OrgErrorCode implements BaseErrorCode { | ||
| //400 | ||
| // 400 | ||
| ORG_NAME_DUPLICATE(HttpStatus.BAD_REQUEST, "ORG_400_1", "사용자가 이미 속해있는 조직의 이름입니다."), | ||
|
|
||
| //403 | ||
| // 403 | ||
| ORG_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_1", "해당 요청은 조직 생성자만 요청 가능합니다."), | ||
|
|
||
| //404 | ||
| // 404 | ||
| ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."), | ||
|
|
||
| //409 | ||
| ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다.") | ||
| ; | ||
| // 409 | ||
| ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), | ||
|
|
||
| // 409 | ||
| ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."), | ||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 에러는 초대를 수락하려 할 때 이미 조직의 멤버인 경우에 발생합니다. 그런데 메시지 "이미 해당 조직에 초대되어있습니다"는 '초대는 받았지만 아직 수락 전인 대기 상태'처럼 읽힐 수 있어 혼란스럽습니다. 실제 상황을 더 정확하게 전달하는 메시지로 바꾸는 게 좋습니다. ✏️ 제안하는 수정- ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."),
+ ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직의 멤버입니다."),🤖 Prompt for AI Agents |
||
|
|
||
| // 400 | ||
| ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400", "조직 초대 토큰이 만료되었거나 유효하지 않습니다."), | ||
|
|
||
| // 403 | ||
| ORG_INVITATION_FORBIDDEN_USER(HttpStatus.FORBIDDEN, "ORG_INVITATION_403_1", | ||
| "초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하지 않습니다."); | ||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String code; | ||
| private final String message; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,4 +96,27 @@ ResponseEntity<DataResponse<OrgResponse.OrgMemberSliceDTO>> getOrgMembers( | |
| ResponseEntity<DataResponse<OrgResponse.OrgMemberCountDTO>> getOrgMembersCount( | ||
| @PathVariable Long orgId | ||
| ); | ||
| } | ||
|
|
||
| @Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "200", description = "성공"), | ||
| @ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음"), | ||
| @ApiResponse(responseCode = "409", description = "이미 조직에 가입된 사용자") | ||
| }) | ||
| public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> sendOrgInvitation( | ||
| @PathVariable Long orgId, | ||
| @RequestBody @Valid OrgRequest.Invite request | ||
| ); | ||
jinnieusLab marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Operation(summary = "조직 초대 수락 API", description = "이메일로 받은 초대 토큰을 통해 조직 가입을 수락합니다. (로그인 필수, 본인 확인)") | ||
| @ApiResponses({ | ||
| @ApiResponse(responseCode = "200", description = "성공"), | ||
| @ApiResponse(responseCode = "400", description = "유효하지 않거나 만료된 토큰"), | ||
| @ApiResponse(responseCode = "401", description = "로그인 필요"), | ||
| @ApiResponse(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치") | ||
| }) | ||
| public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> acceptOrgInvitation( | ||
| @AuthenticationPrincipal(expression = "userId") Long userId, | ||
| @PathVariable String token | ||
| ); | ||
|
Comment on lines
+152
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
✏️ 수정 제안 `@ApiResponses`({
`@ApiResponse`(responseCode = "200", description = "성공"),
`@ApiResponse`(responseCode = "400", description = "유효하지 않거나 만료된 토큰"),
`@ApiResponse`(responseCode = "401", description = "로그인 필요"),
- `@ApiResponse`(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치")
+ `@ApiResponse`(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치"),
+ `@ApiResponse`(responseCode = "409", description = "이미 해당 조직의 멤버인 경우")
})🤖 Prompt for AI Agents |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.