From dd971ed3993d4eb8645155acd7ee43166919d7f9 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Wed, 18 Feb 2026 03:28:11 +0900 Subject: [PATCH 01/17] =?UTF-8?q?:sparkles:=20feat:=20=EC=A1=B0=EC=A7=81?= =?UTF-8?q?=20=EB=A7=B4=EB=B2=84=20=EC=B4=88=EB=8C=80=20-=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/response/OrgResponse.java | 5 ++++ .../domain/service/OrgService.java | 2 ++ .../domain/service/OrgServiceImpl.java | 28 +++++++++++++++++++ .../exception/code/OrgErrorCode.java | 7 +++-- .../repository/OrgMemberRepository.java | 2 ++ .../presentation/OrgController.java | 7 +++++ .../user/domain/service/EmailService.java | 5 ++++ .../user/presentation/AuthController.java | 1 + 8 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.java index cc70538..8913dd9 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.java @@ -27,4 +27,9 @@ public record Delete ( String message ) {} + public record OrgInvitationResponse( + Long orgId, + String message, + String email + ) {} } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java index 052c1d1..f7362f6 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java @@ -16,4 +16,6 @@ public interface OrgService { void removeOrganizationSoft(Long userId, Long orgId); OrgResponse.Delete restoreOrganization(Long userId, Long orgId); + + OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index e6db79b..a97fd89 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -11,6 +11,7 @@ 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; @@ -30,6 +31,8 @@ public class OrgServiceImpl implements OrgService{ private final OrgMemberRepository orgMemberRepository; private final UserRepository userRepository; + private final EmailService emailService; + //조직(워크스페이스) 생성 메서드 public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request) { @@ -132,4 +135,29 @@ public void removeOrganizationSoft(Long userId, Long orgId) { //조직 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)); + + // 유저 회원 가입 유무 + User user = userRepository.findUserByEmail(email).orElseThrow(() -> + new UserHandler(UserErrorCode.USER_NOT_FOUND)); + + // 회원 가입 미완료 (유저가 아닐 시) + if (user == null) { + emailService.sendEmailForOrgInvitation(email); + } + + // 회원 가입 완료 (이미 존재하는 유저) + else { + // 이미 초대 완료 + if (orgMemberRepository.existsByUser(user)) + new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); + + else emailService.sendEmailForOrgInvitation(email); + } + return new OrgResponse.OrgInvitationResponse(orgId, "조직 멤버 초대 이메일을 전송하였습니다.", email); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java index 1204834..0336b65 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java @@ -18,8 +18,11 @@ public enum OrgErrorCode implements BaseErrorCode { ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."), //409 - ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다.") - ; + ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), + + //409 + ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."); + private final HttpStatus httpStatus; private final String code; private final String message; diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java index 0b70803..e6ed1ef 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java @@ -17,4 +17,6 @@ public interface OrgMemberRepository extends JpaRepository { //특정 Organization 에 속한 OrgMember 모두 추출하는 메서드 @Query("select om from OrgMember om where om.organization = :organization") List findOrgMemberByOrg(@Param(value = "organization") Organization organization); + + Boolean existsByUser(User user); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index c8a194c..2739e0e 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -4,6 +4,7 @@ import com.whereyouad.WhereYouAd.domains.organization.application.dto.response.OrgResponse; import com.whereyouad.WhereYouAd.domains.organization.domain.service.OrgService; import com.whereyouad.WhereYouAd.domains.organization.presentation.docs.OrgControllerDocs; +import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService; import com.whereyouad.WhereYouAd.global.response.DataResponse; import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; @@ -85,4 +86,10 @@ public ResponseEntity> removeOrganization( DataResponse.from("조직이 정상적으로 삭제 처리 되었습니다.") ); } + + @PostMapping("/members/{orgId}/invitation") + public ResponseEntity> sendOrgInvitation(@PathVariable Long orgId, @RequestBody String email) { + OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(orgId, email); + return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java index ad7ff56..34a6d57 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.user.domain.service; +import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgRepository; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse; import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler; import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode; @@ -49,6 +50,10 @@ public EmailSentResponse sendEmailForPwd(String toEmail) { } } + // 조직 멤버 초대 이메일 발송 + public void sendEmailForOrgInvitation(String email) { + } + //기존 이메일 발송 로직 템플릿 화 private EmailSentResponse emailSendTemplate(String toEmail, String type) { diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/AuthController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/AuthController.java index e34e374..cdda499 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/AuthController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/AuthController.java @@ -2,6 +2,7 @@ import com.whereyouad.WhereYouAd.domains.user.application.dto.request.LoginRequest; import com.whereyouad.WhereYouAd.domains.user.domain.service.AuthService; +import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService; import com.whereyouad.WhereYouAd.domains.user.presentation.docs.AuthControllerDocs; import com.whereyouad.WhereYouAd.global.response.DataResponse; import com.whereyouad.WhereYouAd.global.security.jwt.dto.TokenResponse; From 99215bb7862e4816d7a014552ca7a34d9b841a7f Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 00:45:34 +0900 Subject: [PATCH 02/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=A1=B0?= =?UTF-8?q?=EC=A7=81=20=EB=A7=B4=EB=B2=84=20=EC=B4=88=EB=8C=80=20uuid?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/OrgService.java | 2 + .../domain/service/OrgServiceImpl.java | 35 +++++--- .../presentation/OrgController.java | 6 ++ .../user/domain/service/EmailService.java | 88 +++++++++++-------- 4 files changed, 79 insertions(+), 52 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java index f7362f6..0203d9c 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java @@ -18,4 +18,6 @@ public interface OrgService { OrgResponse.Delete restoreOrganization(Long userId, Long orgId); OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email); + + OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index a97fd89..8cfd581 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -16,11 +16,13 @@ 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 @@ -31,6 +33,7 @@ public class OrgServiceImpl implements OrgService{ private final OrgMemberRepository orgMemberRepository; private final UserRepository userRepository; + private final RedisUtil redisUtil; private final EmailService emailService; //조직(워크스페이스) 생성 메서드 @@ -137,27 +140,31 @@ public void removeOrganizationSoft(Long userId, Long orgId) { } @Override + // 조직 초대 이메일 보내기 public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email) { Organization organization = orgRepository.findById(orgId).orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - // 유저 회원 가입 유무 - User user = userRepository.findUserByEmail(email).orElseThrow(() -> - new UserHandler(UserErrorCode.USER_NOT_FOUND)); - - // 회원 가입 미완료 (유저가 아닐 시) - if (user == null) { - emailService.sendEmailForOrgInvitation(email); - } - - // 회원 가입 완료 (이미 존재하는 유저) - else { - // 이미 초대 완료 + // 초대 완료 여부 확인, 가입 여부에 상관 없이 이메일 발송 + userRepository.findUserByEmail(email).ifPresent( user -> { if (orgMemberRepository.existsByUser(user)) new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); + }); + + // Redis key = 임의의 UUID 토큰(조직 초대 이메일 내 링크를 구별) + String token = UUID.randomUUID().toString(); + // Redis value = 조직 아이디와 이메일의 조합 + String value = orgId + ":" + email; + redisUtil.setDataExpire(token, value,3600*24); - else emailService.sendEmailForOrgInvitation(email); - } return new OrgResponse.OrgInvitationResponse(orgId, "조직 멤버 초대 이메일을 전송하였습니다.", email); } + + @Override + // 조직 초대 수락 (이메일 내 링크 클릭 시) + public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { + +// orgMemberRepository.save(); + return null; + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index 2739e0e..4d04e64 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -92,4 +92,10 @@ public ResponseEntity> sendOrgIn OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(orgId, email); return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } + + @GetMapping("invitations/{token}") + public ResponseEntity> acceptOrgInvitation(@PathVariable String token) { + OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(token); + return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java index 34a6d57..1dd766d 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java @@ -25,14 +25,14 @@ public class EmailService { private final RedisUtil redisUtil; private final UserRepository userRepository; - //application.yml 적용 필요 + // application.yml 적용 필요 @Value("${spring.mail.username}") private String senderEmail; - //인증코드 이메일 발송 로직 (최초 회원가입 시) + // 인증코드 이메일 발송 로직 (최초 회원가입 시) public EmailSentResponse sendEmail(String toEmail) { - if (userRepository.existsByEmail(toEmail)) { //이미 해당 이메일로 생성한 계정이 있으면 - throw new UserHandler(UserErrorCode.USER_EMAIL_DUPLICATE); //이메일 중복 예외(회원가입 시 사용했던 예외) + if (userRepository.existsByEmail(toEmail)) { // 이미 해당 이메일로 생성한 계정이 있으면 + throw new UserHandler(UserErrorCode.USER_EMAIL_DUPLICATE); // 이메일 중복 예외(회원가입 시 사용했던 예외) } String type = "회원가입"; @@ -40,91 +40,103 @@ public EmailSentResponse sendEmail(String toEmail) { return emailSendTemplate(toEmail, type); } - //비밀번호 재설정을 위한 인증코드 이메일 발송 로직 (이미 회원가입 된 상태에서 비밀번호 재설정) + // 비밀번호 재설정을 위한 인증코드 이메일 발송 로직 (이미 회원가입 된 상태에서 비밀번호 재설정) public EmailSentResponse sendEmailForPwd(String toEmail) { - if (userRepository.existsByEmail(toEmail)) { //이미 회원가입 되어있는 것이 확인되면 + if (userRepository.existsByEmail(toEmail)) { // 이미 회원가입 되어있는 것이 확인되면 String type = "비밀번호 재설정"; - return emailSendTemplate(toEmail, type); //정상적으로 이메일 발송 - } else { //만약 회원가입 되어있지 않다면 - throw new UserHandler(UserErrorCode.USER_NOT_FOUND); //예외발생 + return emailSendTemplate(toEmail, type); // 정상적으로 이메일 발송 + } else { // 만약 회원가입 되어있지 않다면 + throw new UserHandler(UserErrorCode.USER_NOT_FOUND); // 예외발생 } } // 조직 멤버 초대 이메일 발송 - public void sendEmailForOrgInvitation(String email) { + public void sendEmailForOrgInvitation(String toEmail) { + try { + // 이메일 전송 + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(toEmail); + + message.setSubject("Where You Ad 조직에 초대 되었습니다."); + message.setText("http://localhost:3000/invitations/{token}"); + message.setFrom(senderEmail); + + emailSender.send(message); + } catch (MailException e) { // 예외 발생 + throw new UserHandler(UserErrorCode.USER_EMAIL_NOT_VALID); // 통합 응답 처리 예외로 반환 + } } - //기존 이메일 발송 로직 템플릿 화 + // 기존 이메일 발송 로직 템플릿 화 private EmailSentResponse emailSendTemplate(String toEmail, String type) { - //인증코드 재전송 로직 -> 이미 Redis 에 해당 이메일 인증코드가 있을시 삭제 + // 인증코드 재전송 로직 -> 이미 Redis 에 해당 이메일 인증코드가 있을시 삭제 String redisKey = "CODE:" + toEmail; if (redisUtil.getData(redisKey) != null) { redisUtil.deleteData(redisKey); } - String authCode = createCode(); //인증코드 생성 + String authCode = createCode(); // 인증코드 생성 if (isTestEmail(toEmail)) { - //테스트용 가짜 이메일은 서버 로그로만 인증코드를 보여주기 - //실제 이메일 발송 X - //존재하지 않는 이메일 주소로 계속 이메일을 보내면 인증코드 전송용 이메일 계정이 스팸처리 될 가능성 존재하여 로그로 남기기 + // 테스트용 가짜 이메일은 서버 로그로만 인증코드를 보여주기 + // 실제 이메일 발송 X + // 존재하지 않는 이메일 주소로 계속 이메일을 보내면 인증코드 전송용 이메일 계정이 스팸처리 될 가능성 존재하여 로그로 남기기 log.warn("{} 이메일 -> 테스트 or 개발용 가짜 이메일 입니다.", toEmail); log.warn("[TEST 모드] 실제 발송을 건너뜁니다."); log.warn("[TEST 모드] 수신자: {}", toEmail); log.warn("[TEST 모드] 인증코드: {}", authCode); - } else { //실제 존재하는 이메일이라면 + } else { // 실제 존재하는 이메일이라면 try { - //실제 인증 코드가 담긴 이메일 전송 + // 실제 인증 코드가 담긴 이메일 전송 SimpleMailMessage message = new SimpleMailMessage(); message.setTo(toEmail); - //어떤 유형의 인증(최초 회원가입 or 비밀번호 재설정) 인지 구분하여 인증코드 발송 + // 어떤 유형의 인증(최초 회원가입 or 비밀번호 재설정) 인지 구분하여 인증코드 발송 message.setSubject("whereyouad " + type + " 인증번호"); - message.setText("[Where You Ad] " + type + "\n 인증 번호는 [" + authCode + "] 입니다."); + message.setText("[Where You Ad] " + type + "\n 인증 번호는 [" + authCode + "] 입니다."); message.setFrom(senderEmail); - emailSender.send(message); //만약 실제 존재하는 이메일인데 사용자가 오타를 냈다면 - } catch (MailException e) { //예외 발생 - throw new UserHandler(UserErrorCode.USER_EMAIL_NOT_VALID); //통합 응답 처리 예외로 반환 + emailSender.send(message); // 만약 실제 존재하는 이메일인데 사용자가 오타를 냈다면 + } catch (MailException e) { // 예외 발생 + throw new UserHandler(UserErrorCode.USER_EMAIL_NOT_VALID); // 통합 응답 처리 예외로 반환 } - } - //Redis에 저장 (Key: "CODE:이메일", Value: "123456", 유효시간: 180초(3분)) - //테스트 계정도 인증은 해야하니 Redis 에 코드가 저장 되어야 함. - //테스트 계정의 인증은 서버 로그를 통해 인증코드를 얻어 입력. + // Redis에 저장 (Key: "CODE:이메일", Value: "123456", 유효시간: 180초(3분)) + // 테스트 계정도 인증은 해야하니 Redis 에 코드가 저장 되어야 함. + // 테스트 계정의 인증은 서버 로그를 통해 인증코드를 얻어 입력. redisUtil.setDataExpire("CODE:" + toEmail, authCode, 60 * 3L); return new EmailSentResponse("인증코드를 이메일로 전송했습니다.", toEmail, 180L); } - //인증코드 검증 메서드 + // 인증코드 검증 메서드 public void verifyEmailCode(String email, String inputCode) { - //해당 이메일 값으로 Redis 에서 조회 + // 해당 이메일 값으로 Redis 에서 조회 String key = "CODE:" + email; String savedCode = redisUtil.getData(key); - //만약 인증코드가 없거나 잘못 입력했다면, + // 만약 인증코드가 없거나 잘못 입력했다면, if (savedCode == null || !savedCode.equals(inputCode)) { - throw new UserHandler(UserErrorCode.USER_EMAIL_AUTH_INVALID); //예외 발생(BAD_REQUEST) + throw new UserHandler(UserErrorCode.USER_EMAIL_AUTH_INVALID); // 예외 발생(BAD_REQUEST) } - //정상적으로 인증코드를 입력했다면, - //기존 Redis 에 저장된 데이터를 지우고, + // 정상적으로 인증코드를 입력했다면, + // 기존 Redis 에 저장된 데이터를 지우고, redisUtil.deleteData(key); - //"해당 이메일이 정상적으로 인증되었다" 는 값을 다시 Redis 에 저장 -> 이후 회원가입(signup) 내부 로직에서 활용 - redisUtil.setDataExpire("VERIFIED:" + email, "TRUE", 60 * 60L); //1시간 뒤 Expire + // "해당 이메일이 정상적으로 인증되었다" 는 값을 다시 Redis 에 저장 -> 이후 회원가입(signup) 내부 로직에서 활용 + redisUtil.setDataExpire("VERIFIED:" + email, "TRUE", 60 * 60L); // 1시간 뒤 Expire } - //테스트용 이메일은, "test" 로 시작하거나, "example.com" 으로 끝나야 한다. + // 테스트용 이메일은, "test" 로 시작하거나, "example.com" 으로 끝나야 한다. private boolean isTestEmail(String email) { return email.startsWith("test") || email.endsWith("example.com"); } - //무작위 인증코드 값 생성 + // 무작위 인증코드 값 생성 private String createCode() { - return String.valueOf((int)(Math.random() * (900000)) + 100000); + return String.valueOf((int) (Math.random() * (900000)) + 100000); } } From 8e49ab3601fde59b94a7ad8dd9b892308747739a Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:02:47 +0900 Subject: [PATCH 03/17] =?UTF-8?q?:sparkles:=20feat:=20=EC=A1=B0=EC=A7=81?= =?UTF-8?q?=20=EB=A9=A4=EB=B2=84=20=EC=B4=88=EB=8C=80=20=EC=88=98=EB=9D=BD?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/domain/service/OrgServiceImpl.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index 8cfd581..46e35a7 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -163,8 +163,17 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em @Override // 조직 초대 수락 (이메일 내 링크 클릭 시) public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { + // Redis 내 UUID(key)에 대한 email(value) 비교 + String email = redisUtil.getData(token).split(":")[1]; + Organization organization = orgRepository.findById(Long.parseLong(redisUtil.getData(token).split(":")[0])) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); -// orgMemberRepository.save(); - return null; + User user = userRepository.findUserByEmail(email).orElseThrow(() -> + // 회원이 아닐 시 + new UserHandler(UserErrorCode.USER_NOT_FOUND)); + + orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); + + return new OrgResponse.OrgInvitationResponse(organization.getId(), "조직 멤버 초대 이메일을 수락하였습니다.", email); } } From 8d0eb8d230888977194244a4a1b9c12b244feef9 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:13:17 +0900 Subject: [PATCH 04/17] =?UTF-8?q?:wrench:=20chore:=20=EC=A1=B0=EC=A7=81=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20=EA=B4=80=EB=A0=A8=20Redis=20key=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/OrgServiceImpl.java | 68 ++++++++++--------- .../exception/code/OrgErrorCode.java | 10 +-- .../user/domain/service/EmailService.java | 6 +- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index 46e35a7..6930910 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -27,7 +27,7 @@ @Service @Transactional @RequiredArgsConstructor -public class OrgServiceImpl implements OrgService{ +public class OrgServiceImpl implements OrgService { private final OrgRepository orgRepository; private final OrgMemberRepository orgMemberRepository; @@ -36,27 +36,27 @@ public class OrgServiceImpl implements OrgService{ 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 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); @@ -66,24 +66,24 @@ 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); } @@ -91,62 +91,62 @@ 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 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)); + Organization organization = orgRepository.findById(orgId) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); // 초대 완료 여부 확인, 가입 여부에 상관 없이 이메일 발송 - userRepository.findUserByEmail(email).ifPresent( user -> { + userRepository.findUserByEmail(email).ifPresent(user -> { if (orgMemberRepository.existsByUser(user)) new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); }); @@ -155,7 +155,9 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em String token = UUID.randomUUID().toString(); // Redis value = 조직 아이디와 이메일의 조합 String value = orgId + ":" + email; - redisUtil.setDataExpire(token, value,3600*24); + redisUtil.setDataExpire("INVITE:" + token, value, 3600 * 24L); + + emailService.sendEmailForOrgInvitation(token, email, organization.getName()); return new OrgResponse.OrgInvitationResponse(orgId, "조직 멤버 초대 이메일을 전송하였습니다.", email); } @@ -164,7 +166,7 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em // 조직 초대 수락 (이메일 내 링크 클릭 시) public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { // Redis 내 UUID(key)에 대한 email(value) 비교 - String email = redisUtil.getData(token).split(":")[1]; + String email = redisUtil.getData(token.split(":")[1]).split(":")[1]; Organization organization = orgRepository.findById(Long.parseLong(redisUtil.getData(token).split(":")[0])) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java index 0336b65..8aada40 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java @@ -8,19 +8,19 @@ @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 + // 409 ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), - //409 + // 409 ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java index 1dd766d..09e7f44 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java @@ -51,14 +51,14 @@ public EmailSentResponse sendEmailForPwd(String toEmail) { } // 조직 멤버 초대 이메일 발송 - public void sendEmailForOrgInvitation(String toEmail) { + public void sendEmailForOrgInvitation(String token, String toEmail, String orgName) { try { // 이메일 전송 SimpleMailMessage message = new SimpleMailMessage(); message.setTo(toEmail); - message.setSubject("Where You Ad 조직에 초대 되었습니다."); - message.setText("http://localhost:3000/invitations/{token}"); + message.setSubject("[Where You Ad] 조직 " + orgName + "에 초대 되었습니다."); + message.setText("http://localhost:3000/invitations/"+ token); message.setFrom(senderEmail); emailSender.send(message); From 2835466d9c77f7e33545b9603aaf22a6fd551abd Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:20:53 +0900 Subject: [PATCH 05/17] =?UTF-8?q?:wrench:=20chore:=20Redis=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/organization/domain/service/OrgServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index 6930910..fb71b61 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -166,7 +166,7 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em // 조직 초대 수락 (이메일 내 링크 클릭 시) public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { // Redis 내 UUID(key)에 대한 email(value) 비교 - String email = redisUtil.getData(token.split(":")[1]).split(":")[1]; + String email = redisUtil.getData(token).split(":")[1]; Organization organization = orgRepository.findById(Long.parseLong(redisUtil.getData(token).split(":")[0])) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); @@ -176,6 +176,9 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); + // Redis 사용 토큰 삭제 + redisUtil.deleteData(token); + return new OrgResponse.OrgInvitationResponse(organization.getId(), "조직 멤버 초대 이메일을 수락하였습니다.", email); } } From 61a343cc89d80ef73ecf1fc004804601d6ea6c6b Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:32:04 +0900 Subject: [PATCH 06/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=A1=B0?= =?UTF-8?q?=EC=A7=81=20=EC=B4=88=EB=8C=80=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/OrgServiceImpl.java | 20 ++++++++++++++++--- .../exception/code/OrgErrorCode.java | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index fb71b61..e8db0e7 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -165,19 +165,33 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em @Override // 조직 초대 수락 (이메일 내 링크 클릭 시) public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { + // 링크 만료 또는 유효하지 않을 시 + if (token == null) + throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); + // Redis 내 UUID(key)에 대한 email(value) 비교 - String email = redisUtil.getData(token).split(":")[1]; - Organization organization = orgRepository.findById(Long.parseLong(redisUtil.getData(token).split(":")[0])) + String value = redisUtil.getData("INVITE:" + token); + if (value == null) { + throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); + } + + String email = value.split(":")[1]; + + Organization organization = orgRepository.findById(Long.parseLong(value.split(":")[0])) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); User user = userRepository.findUserByEmail(email).orElseThrow(() -> // 회원이 아닐 시 new UserHandler(UserErrorCode.USER_NOT_FOUND)); + // 이미 멤버인지 중복 체크 + if (orgMemberRepository.existsByUser(user)) + throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); + orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); // Redis 사용 토큰 삭제 - redisUtil.deleteData(token); + redisUtil.deleteData("INVITE:" + token); return new OrgResponse.OrgInvitationResponse(organization.getId(), "조직 멤버 초대 이메일을 수락하였습니다.", email); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java index 8aada40..d725ca0 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java @@ -21,7 +21,9 @@ public enum OrgErrorCode implements BaseErrorCode { ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), // 409 - ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."); + ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."), + + ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400" , "조직 초대 토큰이 만료되었거나 유효하지 않습니다."); private final HttpStatus httpStatus; private final String code; From f2008fbc647a0e917e858ab26793793048f76bee Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:34:26 +0900 Subject: [PATCH 07/17] =?UTF-8?q?:wrench:=20chore:=20Redis=20value=20?= =?UTF-8?q?=EA=B0=92=20=EB=B6=84=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/organization/domain/service/OrgServiceImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index e8db0e7..d5652cf 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -175,9 +175,10 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); } - String email = value.split(":")[1]; + String[] valueForSplit = value.split(":"); + String email = valueForSplit[1]; - Organization organization = orgRepository.findById(Long.parseLong(value.split(":")[0])) + Organization organization = orgRepository.findById(Long.parseLong(valueForSplit[0])) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); User user = userRepository.findUserByEmail(email).orElseThrow(() -> From de412918f5ae39c87dd310a1e5e8bfd1337412f9 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 01:57:49 +0900 Subject: [PATCH 08/17] =?UTF-8?q?:wrench:=20chore:=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EB=A1=9C=EC=BB=AC=EA=B3=BC=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=84=9C=EB=B2=84=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/user/domain/service/EmailService.java | 6 ++++-- src/main/resources/application.yml | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java index 09e7f44..ac54b53 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java @@ -1,6 +1,5 @@ package com.whereyouad.WhereYouAd.domains.user.domain.service; -import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgRepository; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse; import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler; import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode; @@ -29,6 +28,9 @@ public class EmailService { @Value("${spring.mail.username}") private String senderEmail; + @Value("${spring.application.base-url}") + private String baseUrl; + // 인증코드 이메일 발송 로직 (최초 회원가입 시) public EmailSentResponse sendEmail(String toEmail) { if (userRepository.existsByEmail(toEmail)) { // 이미 해당 이메일로 생성한 계정이 있으면 @@ -58,7 +60,7 @@ public void sendEmailForOrgInvitation(String token, String toEmail, String orgNa message.setTo(toEmail); message.setSubject("[Where You Ad] 조직 " + orgName + "에 초대 되었습니다."); - message.setText("http://localhost:3000/invitations/"+ token); + message.setText(baseUrl + "/api/org/invitations/" + token); message.setFrom(senderEmail); emailSender.send(message); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ae71d21..2ab573b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,8 @@ server: spring: application: name: WhereYouAd + # 서버 기본 주소 + base-url: ${BASE_URL:http://localhost:8080} # 1. 데이터베이스 설정 datasource: From 2c4842970d414a52f9ca1197f4ec638b22947d57 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 02:29:04 +0900 Subject: [PATCH 09/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=A1=B0?= =?UTF-8?q?=EC=A7=81=20=EC=B4=88=EB=8C=80=20=EC=9A=94=EC=B2=AD=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/application/dto/request/OrgRequest.java | 4 ++++ .../domains/organization/presentation/OrgController.java | 4 ++-- src/main/resources/application.yml | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java index c29ed56..6dcd734 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java @@ -22,4 +22,8 @@ public record Update ( String logoUrl ) {} + public record Invite( + @NotBlank(message = "이메일은 필수입니다.") + String email + ) {} } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index 4d04e64..ae3d050 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -88,8 +88,8 @@ public ResponseEntity> removeOrganization( } @PostMapping("/members/{orgId}/invitation") - public ResponseEntity> sendOrgInvitation(@PathVariable Long orgId, @RequestBody String email) { - OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(orgId, email); + public ResponseEntity> sendOrgInvitation(@PathVariable Long orgId, @RequestBody @Valid OrgRequest.Invite request) { + OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(orgId, request.email()); return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2ab573b..8a88ab3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,8 +4,8 @@ server: spring: application: name: WhereYouAd - # 서버 기본 주소 - base-url: ${BASE_URL:http://localhost:8080} + # 서버 기본 주소 + base-url: ${BASE_URL:http://localhost:8080} # 1. 데이터베이스 설정 datasource: From efc79eea75632bf30b433ba61908d36868e12f9f Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 02:44:46 +0900 Subject: [PATCH 10/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=88=98=EB=9D=BD=20=EC=9C=A0=EC=A0=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/domain/service/OrgService.java | 2 +- .../domain/service/OrgServiceImpl.java | 15 ++++++++++----- .../organization/presentation/OrgController.java | 6 +++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java index 0203d9c..daadd24 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java @@ -19,5 +19,5 @@ public interface OrgService { OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email); - OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token); + OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index d5652cf..54b9ce7 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -164,7 +164,7 @@ public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String em @Override // 조직 초대 수락 (이메일 내 링크 클릭 시) - public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { + public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token) { // 링크 만료 또는 유효하지 않을 시 if (token == null) throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); @@ -178,13 +178,18 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(String token) { String[] valueForSplit = value.split(":"); String email = valueForSplit[1]; + // 로그인한 사용자 검증 + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); + + // 초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하는지 확인 + if (!user.getEmail().equals(email)) { + throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); + } + Organization organization = orgRepository.findById(Long.parseLong(valueForSplit[0])) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - User user = userRepository.findUserByEmail(email).orElseThrow(() -> - // 회원이 아닐 시 - new UserHandler(UserErrorCode.USER_NOT_FOUND)); - // 이미 멤버인지 중복 체크 if (orgMemberRepository.existsByUser(user)) throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index ae3d050..7b135f4 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -4,7 +4,7 @@ import com.whereyouad.WhereYouAd.domains.organization.application.dto.response.OrgResponse; import com.whereyouad.WhereYouAd.domains.organization.domain.service.OrgService; import com.whereyouad.WhereYouAd.domains.organization.presentation.docs.OrgControllerDocs; -import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService; + import com.whereyouad.WhereYouAd.global.response.DataResponse; import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; @@ -94,8 +94,8 @@ public ResponseEntity> sendOrgIn } @GetMapping("invitations/{token}") - public ResponseEntity> acceptOrgInvitation(@PathVariable String token) { - OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(token); + public ResponseEntity> acceptOrgInvitation(@AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable String token) { + OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(userId, token); return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } } From 08144a973a699af30f7a12684b5365e8b5249002 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 02:50:35 +0900 Subject: [PATCH 11/17] =?UTF-8?q?:memo:=20docs:=20=EC=A1=B0=EC=A7=81=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20Swagger=20=EB=AA=85=EC=84=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/docs/OrgControllerDocs.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java index 341a8ec..cbdec6a 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java @@ -69,4 +69,27 @@ public ResponseEntity> removeOrganization( @PathVariable Long orgId, @RequestParam(defaultValue = "false") boolean isHard ); + + @Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음"), + @ApiResponse(responseCode = "409", description = "이미 조직에 가입된 사용자") + }) + public ResponseEntity> sendOrgInvitation( + @PathVariable Long orgId, + @RequestBody @Valid OrgRequest.Invite request + ); + + @Operation(summary = "조직 초대 수락 API", description = "이메일로 받은 초대 토큰을 통해 조직 가입을 수락합니다. (로그인 필수, 본인 확인)") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "유효하지 않거나 만료된 토큰"), + @ApiResponse(responseCode = "401", description = "로그인 필요"), + @ApiResponse(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치") + }) + public ResponseEntity> acceptOrgInvitation( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable String token + ); } From 7a50428cd27ec19ab38aaeb711cf8b4e42ec8237 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 03:00:22 +0900 Subject: [PATCH 12/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=88=98=EB=9D=BD=20=EC=9C=A0=EC=A0=80=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/domain/service/OrgServiceImpl.java | 2 +- .../domains/organization/exception/code/OrgErrorCode.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index 54b9ce7..b998811 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -184,7 +184,7 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String // 초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하는지 확인 if (!user.getEmail().equals(email)) { - throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID); + throw new OrgHandler(OrgErrorCode.ORG_INVITATION_FORBIDDEN_USER); } Organization organization = orgRepository.findById(Long.parseLong(valueForSplit[0])) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java index d725ca0..4b67e52 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java @@ -23,7 +23,12 @@ public enum OrgErrorCode implements BaseErrorCode { // 409 ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."), - ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400" , "조직 초대 토큰이 만료되었거나 유효하지 않습니다."); + // 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; From 1892a75eb3b9d06df51edc98e3aeb5a7667a0a5e Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 04:11:17 +0900 Subject: [PATCH 13/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EC=9E=90=20=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/OrgService.java | 2 +- .../domain/service/OrgServiceImpl.java | 18 ++++++++++++++---- .../repository/OrgMemberRepository.java | 2 +- .../presentation/docs/OrgControllerDocs.java | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java index daadd24..2d994e3 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java @@ -17,7 +17,7 @@ public interface OrgService { OrgResponse.Delete restoreOrganization(Long userId, Long orgId); - OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email); + OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email); OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index b998811..d129ff9 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -141,14 +141,24 @@ public void removeOrganizationSoft(Long userId, Long orgId) { @Override // 조직 초대 이메일 보내기 - public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long orgId, String email) { + public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email) { Organization organization = orgRepository.findById(orgId) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); + // 초대자와 조직 관계 검증 (초대자가 조직의 멤버인지 확인) + User sender = userRepository.findById(userId) + .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); + + if (!orgMemberRepository.existsByUserAndOrganization(sender, organization)) { + // 초대자가 조직 멤버가 아님 -> 권한 없음 + throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); + } + // 초대 완료 여부 확인, 가입 여부에 상관 없이 이메일 발송 userRepository.findUserByEmail(email).ifPresent(user -> { - if (orgMemberRepository.existsByUser(user)) - new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); + if (orgMemberRepository.existsByUserAndOrganization(user, organization)) { + throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); + } }); // Redis key = 임의의 UUID 토큰(조직 초대 이메일 내 링크를 구별) @@ -191,7 +201,7 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); // 이미 멤버인지 중복 체크 - if (orgMemberRepository.existsByUser(user)) + if (orgMemberRepository.existsByUserAndOrganization(user, organization)) throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java index 0ab16b8..d02072b 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java @@ -45,5 +45,5 @@ int countByOrganizationIdAndUserStatus( @Param("status") UserStatus status ); - Boolean existsByUser(User user); + Boolean existsByUserAndOrganization(User user, Organization organization); } \ No newline at end of file diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java index 3294ea0..f9c4e5c 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java @@ -104,6 +104,7 @@ ResponseEntity> getOrgMembersCount( @ApiResponse(responseCode = "409", description = "이미 조직에 가입된 사용자") }) public ResponseEntity> sendOrgInvitation( + @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId, @RequestBody @Valid OrgRequest.Invite request ); From 77408beaee66d1602a24e11adb6ecd4ef4283eeb Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 04:11:45 +0900 Subject: [PATCH 14/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=B4=88?= =?UTF-8?q?=EB=8C=80=20=EC=88=98=EB=9D=BD=20API=20Post=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/presentation/OrgController.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index c2c6095..df93e9b 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -107,13 +107,17 @@ public ResponseEntity> getOrgMembers } @PostMapping("/members/{orgId}/invitation") - public ResponseEntity> sendOrgInvitation(@PathVariable Long orgId, @RequestBody @Valid OrgRequest.Invite request) { - OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(orgId, request.email()); + public ResponseEntity> sendOrgInvitation( + @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId, + @RequestBody @Valid OrgRequest.Invite request) { + OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(userId, orgId, + request.email()); return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } - @GetMapping("invitations/{token}") - public ResponseEntity> acceptOrgInvitation(@AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable String token) { + @PostMapping("invitations/{token}") + public ResponseEntity> acceptOrgInvitation( + @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable String token) { OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(userId, token); return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } From cad16a97db6c1f5228e7944db569fd2e0436d39a Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 04:15:16 +0900 Subject: [PATCH 15/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=A1=B0?= =?UTF-8?q?=EC=A7=81=20=EC=B4=88=EB=8C=80=20=EC=9A=94=EC=B2=AD=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=ED=98=95=EC=8B=9D=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/application/dto/request/OrgRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java index 6dcd734..e3166dc 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.organization.application.dto.request; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; public class OrgRequest { @@ -24,6 +25,7 @@ public record Update ( public record Invite( @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") String email ) {} } From 474ad2d80df594a13245154df4b0f4a210cfc975 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 04:22:22 +0900 Subject: [PATCH 16/17] =?UTF-8?q?:wrench:=20chore:=20=EC=A1=B0=EC=A7=81=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=20API=20=EA=B4=80=EB=A0=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/organization/presentation/OrgController.java | 3 +-- .../organization/presentation/docs/OrgControllerDocs.java | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index df93e9b..fa9b7c0 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -43,7 +43,6 @@ public ResponseEntity> getOrganizations( ); } - @PatchMapping("/{orgId}") public ResponseEntity> modifyOrganization( @AuthenticationPrincipal(expression = "userId") Long userId, @@ -115,7 +114,7 @@ public ResponseEntity> sendOrgIn return ResponseEntity.ok(DataResponse.from(orgInvitationResponse)); } - @PostMapping("invitations/{token}") + @PostMapping("/invitations/{token}") public ResponseEntity> acceptOrgInvitation( @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable String token) { OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(userId, token); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java index f9c4e5c..7498377 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java @@ -100,6 +100,8 @@ ResponseEntity> getOrgMembersCount( @Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "401", description = "로그인 필요"), + @ApiResponse(responseCode = "403", description = "조직 멤버가 아닌 사용자의 요청"), @ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음"), @ApiResponse(responseCode = "409", description = "이미 조직에 가입된 사용자") }) From 9e5bdf9e1b5073b84df5767f154f9027aabb03a0 Mon Sep 17 00:00:00 2001 From: jinnieusLab Date: Thu, 19 Feb 2026 15:54:49 +0900 Subject: [PATCH 17/17] =?UTF-8?q?:recycle:=20refactor:=20=EC=A1=B0?= =?UTF-8?q?=EC=A7=81=20=EC=B4=88=EB=8C=80=20=EC=88=98=EB=9D=BD=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B6=8C=ED=95=9C=20MEMBER=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/OrgMemberConverter.java | 15 +++++++++++++-- .../domain/service/OrgServiceImpl.java | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgMemberConverter.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgMemberConverter.java index 73f5f16..42ec122 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgMemberConverter.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgMemberConverter.java @@ -5,14 +5,25 @@ import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization; import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User; +import java.time.LocalDateTime; + public class OrgMemberConverter { public static OrgMember toOrgMemberADMIN(User user, Organization organization) { return OrgMember.builder() .user(user) .organization(organization) - .joinedAt(organization.getCreatedAt()) //생성한 사람의 조직 합류 시간은 조직 생성 시간과 동일 - .role(OrgRole.ADMIN) //생성한 사람은 ADMIN + .joinedAt(organization.getCreatedAt()) // 생성한 사람의 조직 합류 시간은 조직 생성 시간과 동일 + .role(OrgRole.ADMIN) // 생성한 사람은 ADMIN + .build(); + } + + public static OrgMember toOrgMemberMEMBER(User user, Organization organization) { + return OrgMember.builder() + .user(user) + .organization(organization) + .joinedAt(LocalDateTime.now()) // 조직 초대 완료 시점 + .role(OrgRole.MEMBER) // 초대된 사람은 MEMBER (default) .build(); } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index d129ff9..b63073c 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -204,7 +204,7 @@ public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String if (orgMemberRepository.existsByUserAndOrganization(user, organization)) throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE); - orgMemberRepository.save(OrgMemberConverter.toOrgMemberADMIN(user, organization)); + orgMemberRepository.save(OrgMemberConverter.toOrgMemberMEMBER(user, organization)); // Redis 사용 토큰 삭제 redisUtil.deleteData("INVITE:" + token);