diff --git a/src/main/java/org/example/tackit/config/CommonDataInitializer.java b/src/main/java/org/example/tackit/config/CommonDataInitializer.java index 585cf93..120503e 100644 --- a/src/main/java/org/example/tackit/config/CommonDataInitializer.java +++ b/src/main/java/org/example/tackit/config/CommonDataInitializer.java @@ -1,50 +1,50 @@ package org.example.tackit.config; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.organization.repository.SchoolRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.auth.login.repository.MemberRepository; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.Org.School; import org.example.tackit.domain.entity.Org.SchoolType; +import org.example.tackit.domain.member.repository.MemberRepository; +import org.example.tackit.domain.organization.repository.SchoolRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; - @Component @RequiredArgsConstructor public class CommonDataInitializer implements CommandLineRunner { - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - private final SchoolRepository schoolRepository; - - // commandLineRunner의 run 메서드는 String... args로 유지 (String[] args와 유사) -> 스프링 부트 공식 문서 참고 - // args를 꼭 배열로 넘겨야 하는 건 아니고 가변적으로 받을 수 있다는 의도를 나타냄 - @Override - public void run(String... args) throws Exception { - if (memberRepository.findByEmail("contact.tackit@gmail.com").isEmpty()) { - Member admin = Member.builder() - .email("contact.tackit@gmail.com") - .password(passwordEncoder.encode("admin1")) // BCrypt 인코딩 - .name("관리자") - .status(AccountStatus.ACTIVE) - .createdAt(LocalDateTime.now()) - .build(); - - memberRepository.save(admin); - } + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final SchoolRepository schoolRepository; + + // commandLineRunner의 run 메서드는 String... args로 유지 (String[] args와 유사) -> 스프링 부트 공식 문서 참고 + // args를 꼭 배열로 넘겨야 하는 건 아니고 가변적으로 받을 수 있다는 의도를 나타냄 + @Override + public void run(String... args) throws Exception { + if (memberRepository.findByEmail("contact.tackit@gmail.com").isEmpty()) { + Member admin = Member.builder() + .email("contact.tackit@gmail.com") + .password(passwordEncoder.encode("admin1")) // BCrypt 인코딩 + .name("관리자") + .status(AccountStatus.ACTIVE) + .createdAt(LocalDateTime.now()) + .build(); + + memberRepository.save(admin); + } - if (schoolRepository.findBySchoolName("숙명여자대학교").isEmpty()) { - School sookmyung = School.builder() - .schoolName("숙명여자대학교") - .schoolType(SchoolType.Main) - .regionId(1) - .address("서울특별시 용산구 청파로47길 100") - .build(); + if (schoolRepository.findBySchoolName("숙명여자대학교").isEmpty()) { + School sookmyung = School.builder() + .schoolName("숙명여자대학교") + .schoolType(SchoolType.Main) + .regionId(1) + .address("서울특별시 용산구 청파로47길 100") + .build(); - schoolRepository.save(sookmyung); - } + schoolRepository.save(sookmyung); } + } } diff --git a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java b/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java deleted file mode 100644 index d7c030f..0000000 --- a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberOrgRepository.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.example.tackit.domain.auth.login.repository; - -import org.example.tackit.domain.entity.Member; -import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.entity.Org.OrgStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface MemberOrgRepository extends JpaRepository { - - // 특정 소속(Org) 내에서 닉네임 중복 확인 - boolean existsByOrganizationIdAndNickname(Long orgId, String nickname); - - // 해당 소속(Org)에 가입된 사람이 있는지 확인 (최초 가입자 판별용) - boolean existsByOrganizationId(Long orgId); - - @Query("SELECT mo FROM MemberOrg mo WHERE mo.member.id = :memberId AND mo.id = :profileId") - Optional findByMemberIdAndProfileId(@Param("memberId") Long memberId, @Param("profileId") Long profileId); - - // 멀티 프로필 : 사용자의 이메일로 가입된 모든 프로필 조회 - List findAllByMemberEmail(String email); - - // 사용자의 ID로 가입된 모든 프로필 조회 - List findAllByMemberId(Long memberId); - - // 이메일과 프로필(MemberOrg) ID로 특정 소속 정보 조회 - Optional findByMemberEmailAndId(String email, Long id); - - Optional findByMemberIdAndId(Long memberId, Long profileId); - - // 특정 유저가 해당 프로필의 소유자인지 확인 - boolean existsByMemberIdAndId(Long memberId, Long id); - - // N+1 문제 방지를 위한 페치 조인 (성능 최적화) - @Query("SELECT mo FROM MemberOrg mo JOIN FETCH mo.member WHERE mo.id = :id") - Optional findByIdWithMember(@Param("id") Long id); - - // 조직 ID와 멤버 ID로 가입 여부 확인 (중복 가입 방지용) - boolean existsByMemberIdAndOrganizationId(Long memberId, Long orgId); - - Optional findByMemberIdAndOrganizationId(Long memberId, Long orgId); - - boolean existsByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status); - - Optional findByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status); -} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberRepository.java b/src/main/java/org/example/tackit/domain/auth/login/repository/MemberRepository.java deleted file mode 100644 index aab20c2..0000000 --- a/src/main/java/org/example/tackit/domain/auth/login/repository/MemberRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.tackit.domain.auth.login.repository; - -import org.example.tackit.domain.entity.Member; -import org.example.tackit.domain.entity.AccountStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); //그 유저 실제 정보 추출 - boolean existsByEmail(String email); // 이메일 존재 확인 - // boolean existsByNickname(String nickname); //닉네임 중복 확인 - boolean existsByEmailAndStatus(String email, AccountStatus accountStatus); // 이메일+상태 존재 확인 - Optional findByEmailAndStatus(String email, AccountStatus accountStatus); // 이메일+상태 정보 추출 - - // Optional findByOrganizationAndName(String organization, String name); - - // Optional findByEmailAndOrganization(String email, String organization); - - // Optional findByNameAndOrganizationAndEmail(String name, String organization, String email); -} - diff --git a/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java b/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java index ea97030..bb6add1 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java +++ b/src/main/java/org/example/tackit/domain/auth/login/service/AuthService.java @@ -1,17 +1,24 @@ package org.example.tackit.domain.auth.login.service; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.tackit.config.Redis.RedisUtil; import org.example.tackit.config.jwt.TokenProvider; import org.example.tackit.domain.admin.repository.AdminMemberRepository; -import org.example.tackit.domain.auth.login.dto.*; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.auth.login.dto.MultiProfileDto; +import org.example.tackit.domain.auth.login.dto.SignInDto; +import org.example.tackit.domain.auth.login.dto.SignInResponse; +import org.example.tackit.domain.auth.login.dto.SignUpDto; +import org.example.tackit.domain.auth.login.dto.TokenDto; import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.entity.Org.OrgType; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.member.repository.MemberRepository; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -22,40 +29,35 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - @Slf4j @Service @RequiredArgsConstructor public class AuthService { - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - private final AuthenticationManager authenticationManager; - private final TokenProvider tokenProvider; - private final RedisUtil redisUtil; - private final AdminMemberRepository adminMemberRepository; - private final MemberOrgRepository memberOrgRepository; - - @Transactional - public void signup(SignUpDto signUpDto) { - if (memberRepository.existsByEmail(signUpDto.getEmail())) { - throw new RuntimeException("이미 가입되어 있는 유저입니다"); - } + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final TokenProvider tokenProvider; + private final RedisUtil redisUtil; + private final AdminMemberRepository adminMemberRepository; + private final MemberOrgRepository memberOrgRepository; + + @Transactional + public void signup(SignUpDto signUpDto) { + if (memberRepository.existsByEmail(signUpDto.getEmail())) { + throw new RuntimeException("이미 가입되어 있는 유저입니다"); + } - Member member = Member.builder() - .email(signUpDto.getEmail()) - .password(passwordEncoder.encode(signUpDto.getPassword())) - .name(signUpDto.getName()) - .accountStatus(AccountStatus.ACTIVE) - .createdAt(LocalDateTime.now()) - .build(); + Member member = Member.builder() + .email(signUpDto.getEmail()) + .password(passwordEncoder.encode(signUpDto.getPassword())) + .name(signUpDto.getName()) + .accountStatus(AccountStatus.ACTIVE) + .createdAt(LocalDateTime.now()) + .build(); - memberRepository.save(member); - } + memberRepository.save(member); + } /* @Transactional @@ -77,83 +79,84 @@ public TokenDto signIn(SignInDto signInDto) { } */ - @Transactional - public SignInResponse signIn(SignInDto signInDto) { - // 인증 토큰 생성 - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(signInDto.getEmail(), signInDto.getPassword()); - - try { - log.info("로그인 시도: {}", signInDto.getEmail()); - Authentication authentication = authenticationManager.authenticate(authenticationToken); - log.info("로그인 성공: {}", authentication.getName()); - - TokenDto tokenDto = tokenProvider.generateTokenDto(authentication); - redisUtil.save(signInDto.getEmail(), tokenDto.getRefreshToken()); - - // 멀티 프로필 목록 조회 - List memberOrgs = memberOrgRepository.findAllByMemberEmail(signInDto.getEmail()); - - List profiles = memberOrgs.stream() - .map(org -> MultiProfileDto.builder() - .memberOrgId(org.getId()) - .orgName(org.getOrganization().getName()) - .nickname(org.getNickname()) - .profileImage(org.getProfileImageUrl()) - .orgType(org.getOrgType().name()) - .memberRole(org.getMemberRole().name()) - .memberType(org.getMemberType().name()) - .build()) - .collect(Collectors.toList()); - - return new SignInResponse( - tokenDto, - profiles - ); - } catch (Exception e) { - log.error("로그인 실패: {}", signInDto.getEmail(), e); - throw e; - } - } - - // Bearer 제거 및 형식 검증 - public String resolveBearerToken(String refreshToken) { - if (refreshToken == null || !refreshToken.startsWith("Bearer ")) { - throw new BadCredentialsException("리프레시 토큰이 누락되었거나 올바르지 않습니다."); - } - return refreshToken.substring(7); - } - - // 토큰 재발급 처리 - @Transactional - public TokenDto reissue(String bearerToken) { - String refreshToken = resolveBearerToken(bearerToken); - return tokenProvider.reissueAccessToken(refreshToken); + @Transactional + public SignInResponse signIn(SignInDto signInDto) { + // 인증 토큰 생성 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(signInDto.getEmail(), signInDto.getPassword()); + + try { + log.info("로그인 시도: {}", signInDto.getEmail()); + Authentication authentication = authenticationManager.authenticate(authenticationToken); + log.info("로그인 성공: {}", authentication.getName()); + + TokenDto tokenDto = tokenProvider.generateTokenDto(authentication); + redisUtil.save(signInDto.getEmail(), tokenDto.getRefreshToken()); + + // 멀티 프로필 목록 조회 + List memberOrgs = memberOrgRepository.findAllByMemberEmail(signInDto.getEmail()); + + List profiles = memberOrgs.stream() + .map(org -> MultiProfileDto.builder() + .memberOrgId(org.getId()) + .orgName(org.getOrganization().getName()) + .nickname(org.getNickname()) + .profileImage(org.getProfileImageUrl()) + .orgType(org.getOrgType().name()) + .memberRole(org.getMemberRole().name()) + .memberType(org.getMemberType().name()) + .build()) + .collect(Collectors.toList()); + + return new SignInResponse( + tokenDto, + profiles + ); + } catch (Exception e) { + log.error("로그인 실패: {}", signInDto.getEmail(), e); + throw e; } + } - // 특정 소속 선택 - public SignInResponse selectProfile(Long memberOrgId, String email) { - // 소속 검증 - MemberOrg selectedOrg = memberOrgRepository.findById(memberOrgId) - .filter(org -> org.getMember().getEmail().equals(email)) - .orElseThrow( () -> new RuntimeException("해당 소속 권한이 없거나 존재하지 않습니다.")); - - // 권한 리스트 생성 : Role + Type - List authorities = Arrays.asList( - new SimpleGrantedAuthority("ROLE_" + selectedOrg.getMemberRole().name()), - new SimpleGrantedAuthority("ROLE_" + selectedOrg.getMemberType().name()) - ); - - // 인증 객체 생성 - Authentication authentication = new UsernamePasswordAuthenticationToken(email, null, authorities); - - // MemberOrgId 포함하여 토큰 생성 - TokenDto orgToken = tokenProvider.generateTokenDtoWithProfile(authentication, memberOrgId); - - return SignInResponse.of(orgToken); + // Bearer 제거 및 형식 검증 + public String resolveBearerToken(String refreshToken) { + if (refreshToken == null || !refreshToken.startsWith("Bearer ")) { + throw new BadCredentialsException("리프레시 토큰이 누락되었거나 올바르지 않습니다."); } - - // 이메일 찾기 + return refreshToken.substring(7); + } + + // 토큰 재발급 처리 + @Transactional + public TokenDto reissue(String bearerToken) { + String refreshToken = resolveBearerToken(bearerToken); + return tokenProvider.reissueAccessToken(refreshToken); + } + + // 특정 소속 선택 + public SignInResponse selectProfile(Long memberOrgId, String email) { + // 소속 검증 + MemberOrg selectedOrg = memberOrgRepository.findById(memberOrgId) + .filter(org -> org.getMember().getEmail().equals(email)) + .orElseThrow(() -> new RuntimeException("해당 소속 권한이 없거나 존재하지 않습니다.")); + + // 권한 리스트 생성 : Role + Type + List authorities = Arrays.asList( + new SimpleGrantedAuthority("ROLE_" + selectedOrg.getMemberRole().name()), + new SimpleGrantedAuthority("ROLE_" + selectedOrg.getMemberType().name()) + ); + + // 인증 객체 생성 + Authentication authentication = new UsernamePasswordAuthenticationToken(email, null, + authorities); + + // MemberOrgId 포함하여 토큰 생성 + TokenDto orgToken = tokenProvider.generateTokenDtoWithProfile(authentication, memberOrgId); + + return SignInResponse.of(orgToken); + } + + // 이메일 찾기 /* @Transactional public FindEmailRespDto findEmailbyOrgAndNickname(String organization, String name) { @@ -214,36 +217,36 @@ public ResetTokenDto findPwByIdentity(String name, String organization, String e */ - // 비밀번호 찾기 ) 비밀번호 재설정 - @Transactional - public void resetPassword(String authorizationHeader, String newPassword) { - // 1. 토큰 추출 및 형식 검증 - String resetToken = resolveBearerToken(authorizationHeader); + // 비밀번호 찾기 ) 비밀번호 재설정 + @Transactional + public void resetPassword(String authorizationHeader, String newPassword) { + // 1. 토큰 추출 및 형식 검증 + String resetToken = resolveBearerToken(authorizationHeader); - // 2. JWT 유효성 및 용도 확인 - if( !tokenProvider.validateToken(resetToken) || !tokenProvider.isResetToken(resetToken) ) { - throw new BadCredentialsException("유효하지 않거나 만료된 재설정 토큰입니다."); - } + // 2. JWT 유효성 및 용도 확인 + if (!tokenProvider.validateToken(resetToken) || !tokenProvider.isResetToken(resetToken)) { + throw new BadCredentialsException("유효하지 않거나 만료된 재설정 토큰입니다."); + } - // 3. 토큰에서 이메일 추출 - String email = tokenProvider.getEmailFromToken(resetToken); + // 3. 토큰에서 이메일 추출 + String email = tokenProvider.getEmailFromToken(resetToken); - // 4. Redis에 저장된 토큰과 일치하는지 확인 - String token = redisUtil.getData("reset:" + email); - if( token == null || !token.equals(resetToken) ) { - // 이미 사용되었거나 만료된 토큰 - throw new BadCredentialsException("이미 사용되었거나 유효하지 않은 토큰입니다."); - } + // 4. Redis에 저장된 토큰과 일치하는지 확인 + String token = redisUtil.getData("reset:" + email); + if (token == null || !token.equals(resetToken)) { + // 이미 사용되었거나 만료된 토큰 + throw new BadCredentialsException("이미 사용되었거나 유효하지 않은 토큰입니다."); + } - // 5. 비밀번호 업데이트 - Member member = adminMemberRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(email + " not found")); + // 5. 비밀번호 업데이트 + Member member = adminMemberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException(email + " not found")); - String encodedNewPassword = passwordEncoder.encode(newPassword); - member.changePassword(encodedNewPassword); + String encodedNewPassword = passwordEncoder.encode(newPassword); + member.changePassword(encodedNewPassword); - // 6. Redis에서 토큰 삭제 - redisUtil.delete("reset:" + email); - } + // 6. Redis에서 토큰 삭제 + redisUtil.delete("reset:" + email); + } } diff --git a/src/main/java/org/example/tackit/domain/auth/login/service/CheckService.java b/src/main/java/org/example/tackit/domain/auth/login/service/CheckService.java index 2d30104..5655e08 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/service/CheckService.java +++ b/src/main/java/org/example/tackit/domain/auth/login/service/CheckService.java @@ -1,18 +1,18 @@ package org.example.tackit.domain.auth.login.service; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.repository.MemberRepository; +import org.example.tackit.domain.member.repository.MemberRepository; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class CheckService { - private final MemberRepository memberRepository; + private final MemberRepository memberRepository; - public boolean isEmailDuplicated(String email) { - return memberRepository.existsByEmail(email); - } + public boolean isEmailDuplicated(String email) { + return memberRepository.existsByEmail(email); + } /* public boolean isNicknameDuplicated(String nickname) { diff --git a/src/main/java/org/example/tackit/domain/auth/login/service/CustomUserDetailsService.java b/src/main/java/org/example/tackit/domain/auth/login/service/CustomUserDetailsService.java index 4d44192..d81009a 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/service/CustomUserDetailsService.java +++ b/src/main/java/org/example/tackit/domain/auth/login/service/CustomUserDetailsService.java @@ -1,51 +1,50 @@ package org.example.tackit.domain.auth.login.service; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.repository.MemberRepository; import org.example.tackit.domain.auth.login.security.CustomUserDetails; -import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.member.repository.MemberRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; - import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; - @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { - private final MemberRepository memberRepository; - @Override - @Transactional - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException(email + " -> 데이터베이스에서 찾을 수 없습니다.")); + private final MemberRepository memberRepository; - // 상태 확인 추가 - if (member.getAccountStatus() == AccountStatus.DELETED) { - throw new UsernameNotFoundException(email + " -> 탈퇴한 회원입니다."); - } + @Override + @Transactional + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException(email + " -> 데이터베이스에서 찾을 수 없습니다.")); - return createUserDetails(member); + // 상태 확인 추가 + if (member.getAccountStatus() == AccountStatus.DELETED) { + throw new UsernameNotFoundException(email + " -> 탈퇴한 회원입니다."); } - private UserDetails createUserDetails(Member member) { - // 최소 권한 부여 - List authorities = new ArrayList<>(); - authorities.add(new SimpleGrantedAuthority("ROLE_USER")); - - return new CustomUserDetails( - member.getId(), - member.getEmail(), - member.getPassword(), - authorities - ); - } + return createUserDetails(member); + } + + private UserDetails createUserDetails(Member member) { + // 최소 권한 부여 + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_USER")); + + return new CustomUserDetails( + member.getId(), + member.getEmail(), + member.getPassword(), + authorities + ); + } } diff --git a/src/main/java/org/example/tackit/domain/auth/login/service/RejoinCheckService.java b/src/main/java/org/example/tackit/domain/auth/login/service/RejoinCheckService.java index 95d71d2..275357b 100644 --- a/src/main/java/org/example/tackit/domain/auth/login/service/RejoinCheckService.java +++ b/src/main/java/org/example/tackit/domain/auth/login/service/RejoinCheckService.java @@ -1,37 +1,38 @@ package org.example.tackit.domain.auth.login.service; +import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.member.repository.MemberRepository; import org.springframework.stereotype.Service; -import java.util.Optional; - @Service @RequiredArgsConstructor public class RejoinCheckService { - private final MemberRepository memberRepository; - // 회원 상태 조회 - public String checkEmailStatus(String email) { - if (memberRepository.existsByEmailAndStatus(email, AccountStatus.DELETED)) { - return "DELETED"; - } else if (memberRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE)) { - return "ACTIVE"; - } else { - return "AVAILABLE"; - } + private final MemberRepository memberRepository; + + // 회원 상태 조회 + public String checkEmailStatus(String email) { + if (memberRepository.existsByEmailAndStatus(email, AccountStatus.DELETED)) { + return "DELETED"; + } else if (memberRepository.existsByEmailAndStatus(email, AccountStatus.ACTIVE)) { + return "ACTIVE"; + } else { + return "AVAILABLE"; } + } - // 탈퇴한 회원 삭제 - public boolean deleteDeletedMember(String email) { - Optional deletedMember = memberRepository.findByEmailAndStatus(email, AccountStatus.DELETED); - if (deletedMember.isPresent()) { - memberRepository.delete(deletedMember.get()); - return true; - } - return false; + // 탈퇴한 회원 삭제 + public boolean deleteDeletedMember(String email) { + Optional deletedMember = memberRepository.findByEmailAndStatus(email, + AccountStatus.DELETED); + if (deletedMember.isPresent()) { + memberRepository.delete(deletedMember.get()); + return true; } + return false; + } } diff --git a/src/main/java/org/example/tackit/domain/event/controller/EventController.java b/src/main/java/org/example/tackit/domain/event/controller/EventController.java index ad0b77d..dc8f0c6 100644 --- a/src/main/java/org/example/tackit/domain/event/controller/EventController.java +++ b/src/main/java/org/example/tackit/domain/event/controller/EventController.java @@ -1,125 +1,122 @@ package org.example.tackit.domain.event.controller; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.security.CustomUserDetails; +import org.example.tackit.common.dto.ActiveProfile; +import org.example.tackit.common.dto.ProfileContext; import org.example.tackit.domain.event.dto.EventCreateReqDto; import org.example.tackit.domain.event.dto.EventDetailResDto; import org.example.tackit.domain.event.dto.EventSimpleResDto; import org.example.tackit.domain.event.dto.EventUpdateReqDto; import org.example.tackit.domain.event.service.EventService; +import org.example.tackit.global.response.ApiResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/api/events") public class EventController { - private final EventService eventService; - - /** - * 일정 생성 - */ - @PostMapping - public ResponseEntity createEvent( - @RequestBody EventCreateReqDto reqDto, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - // TODO 인증 정보가 없습니다 코드 공통 로직 처리 - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - Long eventId = eventService.createEvent(reqDto, userDetails.getId()); - - // TODO ResponseEntity 커스텀 공통 양식 추가 - return ResponseEntity.status(HttpStatus.CREATED).body(eventId); - } - - /** - * 일정 수정 - */ - @PatchMapping("/{eventId}") - public ResponseEntity updateEvent( - @PathVariable Long eventId, - @RequestBody EventUpdateReqDto reqDto, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - eventService.updateEvent(eventId, reqDto, userDetails.getId()); - return ResponseEntity.status(HttpStatus.OK).body(eventId); - } - - /** - * 월간 일정 조회 - */ - @GetMapping("/monthly") - public ResponseEntity getMonthlyEvents( - @RequestParam Long orgId, - @RequestParam int year, - @RequestParam int month, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - List events = eventService.getMonthlyEvents(orgId, year, month, userDetails.getId()); - return ResponseEntity.status(HttpStatus.OK).body(events); - } - - /** - * 다가오는 일정(사이드바) 조회 - */ - @GetMapping("/upcoming") - public ResponseEntity getUpcomingEvents( - @RequestParam Long orgId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - List events = eventService.getUpcomingEvents(orgId, userDetails.getId()); - return ResponseEntity.status(HttpStatus.OK).body(events); - } - - /** - * 일정 상세 조회 - */ - @GetMapping("/{eventId:[0-9]+}") - public ResponseEntity getEventDetail( - @PathVariable Long eventId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - EventDetailResDto detail = eventService.getEventDetail(eventId, userDetails.getId()); - return ResponseEntity.status(HttpStatus.OK).body(detail); - } - - /** - * 일정 삭제 - */ - @DeleteMapping("/{eventId}") - public ResponseEntity deleteEvent( - @PathVariable Long eventId, - @AuthenticationPrincipal CustomUserDetails userDetails - ) { - if (userDetails == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다."); - } - - eventService.deleteEvent(eventId, userDetails.getId()); - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); - } + private final EventService eventService; + + /** + * 일정 생성 + */ + @PostMapping + public ResponseEntity> createEvent( + @RequestBody EventCreateReqDto reqDto, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + Long eventId = eventService.createEvent(reqDto, memberOrgId); + + return ApiResponse.success(HttpStatus.CREATED, "일정 생성 성공", eventId); + } + + /** + * 일정 수정 + */ + @PatchMapping("/{eventId}") + public ResponseEntity> updateEvent( + @PathVariable Long eventId, + @RequestBody EventUpdateReqDto reqDto, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + eventService.updateEvent(eventId, reqDto, memberOrgId); + + return ApiResponse.success(HttpStatus.OK, "일정 수정 성공", eventId); + } + + /** + * 월간 일정 조회 + */ + @GetMapping("/monthly") + public ResponseEntity>> getMonthlyEvents( + @RequestParam Long orgId, + @RequestParam int year, + @RequestParam int month, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + + List events = eventService.getMonthlyEvents(orgId, year, month, memberOrgId); + + return ApiResponse.success(HttpStatus.OK, "월간 일정 조회 성공", events); + } + + /** + * 다가오는 일정(사이드바) 조회 + */ + @GetMapping("/upcoming") + public ResponseEntity>> getUpcomingEvents( + @RequestParam Long orgId, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + + List events = eventService.getUpcomingEvents(orgId, memberOrgId); + + return ApiResponse.success(HttpStatus.OK, "다가오는 일정 조회 성공", events); + } + + /** + * 일정 상세 조회 + */ + @GetMapping("/{eventId:[0-9]+}") + public ResponseEntity> getEventDetail( + @PathVariable Long eventId, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + + EventDetailResDto detail = eventService.getEventDetail(eventId, memberOrgId); + + return ApiResponse.success(HttpStatus.OK, "일정 상세 조회 성공", detail); + } + + /** + * 일정 삭제 + */ + @DeleteMapping("/{eventId}") + public ResponseEntity> deleteEvent( + @PathVariable Long eventId, + @ActiveProfile ProfileContext profileContext + ) { + Long memberOrgId = profileContext.id(); + + eventService.deleteEvent(eventId, memberOrgId); + + return ApiResponse.success(HttpStatus.OK, "일정 삭제 성공"); + } } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java index 3f49ec4..8e948b6 100644 --- a/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java +++ b/src/main/java/org/example/tackit/domain/event/dto/EventCreateReqDto.java @@ -1,9 +1,9 @@ package org.example.tackit.domain.event.dto; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.*; import org.example.tackit.domain.entity.EventScope; -import org.example.tackit.domain.entity.Org.OrgType; import java.time.LocalDateTime; import java.util.List; @@ -32,7 +32,7 @@ public class EventCreateReqDto { private EventScope eventScope; @NotNull(message = "참여자 목록 필드는 필수입니다.") - private List participants; + private List participants; @NotBlank(message = "색상 코드는 필수입니다.") private String colorChip; diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java index 3a9e787..bcc3f96 100644 --- a/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java +++ b/src/main/java/org/example/tackit/domain/event/dto/EventDetailResDto.java @@ -1,10 +1,12 @@ package org.example.tackit.domain.event.dto; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; - import java.time.LocalDateTime; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.tackit.domain.member.dto.SimpleMemberProfileDto; @Getter @NoArgsConstructor @@ -12,11 +14,11 @@ @Builder public class EventDetailResDto { - private Long eventId; - private String title; - private LocalDateTime startsAt; - private LocalDateTime endsAt; - private String description; - private String colorChip; - private List participants; + private Long eventId; + private String title; + private LocalDateTime startsAt; + private LocalDateTime endsAt; + private String description; + private String colorChip; + private List participants; } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java deleted file mode 100644 index 8461f46..0000000 --- a/src/main/java/org/example/tackit/domain/event/dto/EventParticipantDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.example.tackit.domain.event.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class EventParticipantDto { - private Long orgMemberId; - private String profileImageUrl; - private String nickname; -} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java b/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java index 928c8b0..750cb62 100644 --- a/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java +++ b/src/main/java/org/example/tackit/domain/event/dto/EventSimpleResDto.java @@ -1,6 +1,10 @@ package org.example.tackit.domain.event.dto; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + import java.time.LocalDateTime; @Getter diff --git a/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java b/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java index e6c81e0..848f89d 100644 --- a/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java +++ b/src/main/java/org/example/tackit/domain/event/repository/EventRepository.java @@ -1,7 +1,6 @@ package org.example.tackit.domain.event.repository; import org.example.tackit.domain.entity.Event; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/src/main/java/org/example/tackit/domain/event/service/EventService.java b/src/main/java/org/example/tackit/domain/event/service/EventService.java index e7129bf..2d32f96 100644 --- a/src/main/java/org/example/tackit/domain/event/service/EventService.java +++ b/src/main/java/org/example/tackit/domain/event/service/EventService.java @@ -1,221 +1,200 @@ package org.example.tackit.domain.event.service; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.Organization.repository.OrganizationRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.*; +import org.example.tackit.domain.entity.Event; +import org.example.tackit.domain.entity.EventParticipant; +import org.example.tackit.domain.entity.Member; import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.entity.Org.OrgStatus; import org.example.tackit.domain.entity.Org.Organization; -import org.example.tackit.domain.event.dto.*; +import org.example.tackit.domain.event.dto.EventCreateReqDto; +import org.example.tackit.domain.event.dto.EventDetailResDto; +import org.example.tackit.domain.event.dto.EventSimpleResDto; +import org.example.tackit.domain.event.dto.EventUpdateReqDto; import org.example.tackit.domain.event.repository.EventRepository; -import org.example.tackit.global.exception.ErrorCode; -import org.example.tackit.global.exception.MemberNotFoundException; -import org.springframework.data.domain.PageRequest; +import org.example.tackit.domain.member.component.MemberOrgValidator; +import org.example.tackit.domain.member.dto.SimpleMemberProfileDto; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.organization.repository.OrganizationRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.YearMonth; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class EventService { - private final EventRepository eventRepository; - private final OrganizationRepository organizationRepository; - private final MemberOrgRepository memberOrgRepository; - - // 일정 생성 - @Transactional - public Long createEvent(EventCreateReqDto reqDto, Long requesterId) { - MemberOrg memberOrg = validateExecutive(reqDto.getOrgId(), requesterId); - - Member creator = memberOrg.getMember(); - - Organization organization = organizationRepository.findById(reqDto.getOrgId()) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 조직입니다.")); - - Event event = Event.builder() - .organization(organization) - .creator(creator) - .title(reqDto.getTitle()) - .startsAt(reqDto.getStartsAt()) - .endsAt(reqDto.getEndsAt()) - .description(reqDto.getDescription()) - .colorChip(reqDto.getColorChip()) - .eventScope(reqDto.getEventScope()) - .build(); - - // 참여자 추가 - addParticipants(event, reqDto.getParticipants()); - - return eventRepository.save(event).getId(); - } - - // 일정 수정 - @Transactional - public void updateEvent(Long eventId, EventUpdateReqDto reqDto, Long requesterId) { - Event event = findEventOrThrow(eventId); - - validateExecutive(event.getOrganization().getId(), requesterId); - - event.update( - reqDto.getTitle(), - reqDto.getStartsAt(), - reqDto.getEndsAt(), - reqDto.getDescription(), - reqDto.getColorChip(), - reqDto.getEventScope() - ); - - // 참여자 목록 수정 (기존의 데이터 전부 삭제 후 다시 추가) - if (reqDto.getParticipants() != null) { - event.clearParticipants(); - addParticipants(event, reqDto.getParticipants()); - } - } - - // 일정 삭제 - @Transactional - public void deleteEvent(Long eventId, Long requesterId) { - Event event = findEventOrThrow(eventId); - - validateExecutive(event.getOrganization().getId(), requesterId); - - eventRepository.delete(event); - } - - // 월간 일정 조회 - public List getMonthlyEvents(Long orgId, int year, int month, Long requesterId) { - validateMembership(orgId, requesterId); - - YearMonth yearMonth = YearMonth.of(year, month); - LocalDateTime startDateTime = yearMonth.atDay(1).atStartOfDay(); - LocalDateTime endDateTime = yearMonth.atEndOfMonth().atTime(LocalTime.MAX); - - List events = eventRepository.findAllByOrganizationIdAndDateRange(orgId, startDateTime, endDateTime); - - return events.stream() - .map(event -> EventSimpleResDto.builder() - .eventId(event.getId()) - .title(event.getTitle()) - .startsAt(event.getStartsAt()) - .endsAt(event.getEndsAt()) - .colorChip(event.getColorChip()) - .build()) - .collect(Collectors.toList()); - } - - // 일정 상세 조회 - public EventDetailResDto getEventDetail(Long eventId, Long requesterId) { - Event event = findEventOrThrow(eventId); - - validateMembership(event.getOrganization().getId(), requesterId); - - List participantDtos = event.getParticipants().stream() - .map(ep -> EventParticipantDto.builder() - .orgMemberId(ep.getMemberOrg().getId()) - .profileImageUrl(ep.getMemberOrg().getProfileImageUrl()) - .nickname(ep.getMemberOrg().getNickname()) - .build()) - .collect(Collectors.toList()); - - return EventDetailResDto.builder() - .eventId(event.getId()) - .title(event.getTitle()) - .startsAt(event.getStartsAt()) - .endsAt(event.getEndsAt()) - .description(event.getDescription()) - .colorChip(event.getColorChip()) - .participants(participantDtos) - .build(); + private final EventRepository eventRepository; + private final OrganizationRepository organizationRepository; + private final MemberOrgRepository memberOrgRepository; + private final MemberOrgValidator memberOrgValidator; + + // 일정 생성 + @Transactional + public Long createEvent(EventCreateReqDto reqDto, Long requesterMemberOrgId) { + MemberOrg memberOrg = memberOrgValidator.validateExecutive(reqDto.getOrgId(), + requesterMemberOrgId); + + Member creator = memberOrg.getMember(); + + Organization organization = organizationRepository.findById(reqDto.getOrgId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 조직입니다.")); + + Event event = Event.builder() + .organization(organization) + .creator(creator) + .title(reqDto.getTitle()) + .startsAt(reqDto.getStartsAt()) + .endsAt(reqDto.getEndsAt()) + .description(reqDto.getDescription()) + .colorChip(reqDto.getColorChip()) + .eventScope(reqDto.getEventScope()) + .build(); + + // 참여자 추가 + addParticipants(event, reqDto.getParticipants()); + + return eventRepository.save(event).getId(); + } + + // 일정 수정 + @Transactional + public void updateEvent(Long eventId, EventUpdateReqDto reqDto, Long requesterMemberOrgId) { + Event event = findEventOrThrow(eventId); + + memberOrgValidator.validateExecutive(event.getOrganization().getId(), requesterMemberOrgId); + + event.update( + reqDto.getTitle(), + reqDto.getStartsAt(), + reqDto.getEndsAt(), + reqDto.getDescription(), + reqDto.getColorChip(), + reqDto.getEventScope() + ); + + // 참여자 목록 수정 (기존의 데이터 전부 삭제 후 다시 추가) + if (reqDto.getParticipants() != null) { + event.clearParticipants(); + addParticipants(event, reqDto.getParticipants()); } - - // 다가오는 일정 조회 - public List getUpcomingEvents(Long orgId, Long requesterId) { - validateMembership(orgId, requesterId); - - List events = eventRepository.findByOrganizationIdAndStartsAtAfterOrderByStartsAtAsc( - orgId, - LocalDateTime.now() - ); - - return events.stream() - .map(event -> EventSimpleResDto.builder() - .eventId(event.getId()) - .title(event.getTitle()) - .startsAt(event.getStartsAt()) - .endsAt(event.getEndsAt()) - .colorChip(event.getColorChip()) - .build()) - .collect(Collectors.toList()); + } + + // 일정 삭제 + @Transactional + public void deleteEvent(Long eventId, Long requesterMemberOrgId) { + Event event = findEventOrThrow(eventId); + + memberOrgValidator.validateExecutive(event.getOrganization().getId(), requesterMemberOrgId); + + eventRepository.delete(event); + } + + // 월간 일정 조회 + public List getMonthlyEvents(Long orgId, int year, int month, + Long requesterMemberOrgId) { + memberOrgValidator.validateActiveMembership(orgId, requesterMemberOrgId); + + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDateTime = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDateTime = yearMonth.atEndOfMonth().atTime(LocalTime.MAX); + + List events = eventRepository.findAllByOrganizationIdAndDateRange(orgId, startDateTime, + endDateTime); + + return events.stream() + .map(event -> EventSimpleResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .colorChip(event.getColorChip()) + .build()) + .collect(Collectors.toList()); + } + + // 일정 상세 조회 + public EventDetailResDto getEventDetail(Long eventId, Long requesterMemberOrgId) { + Event event = findEventOrThrow(eventId); + + memberOrgValidator.validateActiveMembership(event.getOrganization().getId(), + requesterMemberOrgId); + + List participantDtos = event.getParticipants().stream() + .map(ep -> SimpleMemberProfileDto.builder() + .orgMemberId(ep.getMemberOrg().getId()) + .profileImageUrl(ep.getMemberOrg().getProfileImageUrl()) + .nickname(ep.getMemberOrg().getNickname()) + .build()) + .collect(Collectors.toList()); + + return EventDetailResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .description(event.getDescription()) + .colorChip(event.getColorChip()) + .participants(participantDtos) + .build(); + } + + // 다가오는 일정 조회 + public List getUpcomingEvents(Long orgId, Long requesterMemberOrgId) { + memberOrgValidator.validateActiveMembership(orgId, requesterMemberOrgId); + + List events = eventRepository.findByOrganizationIdAndStartsAtAfterOrderByStartsAtAsc( + orgId, + LocalDateTime.now() + ); + + return events.stream() + .map(event -> EventSimpleResDto.builder() + .eventId(event.getId()) + .title(event.getTitle()) + .startsAt(event.getStartsAt()) + .endsAt(event.getEndsAt()) + .colorChip(event.getColorChip()) + .build()) + .collect(Collectors.toList()); + } + + // 이벤트 참가자 추가 메서드 + private void addParticipants(Event event, List memberOrgIds) { + if (memberOrgIds == null || memberOrgIds.isEmpty()) { + return; } - // 이벤트 참가자 추가 메서드 - private void addParticipants(Event event, List memberOrgIds) { - if (memberOrgIds == null || memberOrgIds.isEmpty()) return; - - List memberOrgs = memberOrgRepository.findAllById(memberOrgIds); - - // 개수 검증 - if (memberOrgs.size() != memberOrgIds.size()) { - throw new IllegalArgumentException("존재하지 않는 부원 ID가 포함되어 있습니다."); - } - - for (MemberOrg memberOrg : memberOrgs) { - // 소속 동아리 일치 여부 검증 - if (!memberOrg.getOrganization().getId().equals(event.getOrganization().getId())) { - throw new IllegalArgumentException("해당 동아리의 소속 부원이 아닙니다."); - } - - // 참여자 생성 - EventParticipant participant = EventParticipant.builder() - .event(event) - .memberOrg(memberOrg) - .build(); - - participant.assignEvent(event); - } - } - - // 이벤트 존재 확인 메서드 - private Event findEventOrThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 일정입니다.")); - } + List memberOrgs = memberOrgRepository.findAllById(memberOrgIds); - // 활동 중인 멤버(운영진 포함)인지 확인하는 메서드 - private void validateMembership(Long orgId, Long memberId) { - boolean isActiveMember = memberOrgRepository.existsByMemberIdAndOrganizationIdAndOrgStatus( - memberId, - orgId, - OrgStatus.ACTIVE - ); - - if (!isActiveMember) { - throw new IllegalArgumentException("해당 조직의 활동 중인 회원만 접근할 수 있습니다."); - } + // 개수 검증 + if (memberOrgs.size() != memberOrgIds.size()) { + throw new IllegalArgumentException("존재하지 않는 부원 ID가 포함되어 있습니다."); } - // 활동 중인 운영진인지 확인하는 메서드 - private MemberOrg validateExecutive(Long orgId, Long memberId) { - MemberOrg memberOrg = memberOrgRepository.findByMemberIdAndOrganizationIdAndOrgStatus( - memberId, - orgId, - OrgStatus.ACTIVE - ).orElseThrow(() -> new IllegalArgumentException("해당 조직의 활동 중인 회원만 접근할 수 있습니다.")); + for (MemberOrg memberOrg : memberOrgs) { + // 소속 동아리 일치 여부 검증 + if (!memberOrg.getOrganization().getId().equals(event.getOrganization().getId())) { + throw new IllegalArgumentException("해당 동아리의 소속 부원이 아닙니다."); + } - if (memberOrg.getMemberRole() != MemberRole.EXECUTIVE) { - throw new IllegalArgumentException("해당 조직의 운영진만 일정을 관리할 수 있습니다."); - } + // 참여자 생성 + EventParticipant participant = EventParticipant.builder() + .event(event) + .memberOrg(memberOrg) + .build(); - return memberOrg; + participant.assignEvent(event); } -} + } + + // 이벤트 존재 확인 메서드 + private Event findEventOrThrow(Long eventId) { + return eventRepository.findById(eventId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 일정입니다.")); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/freeBoard/Free_comment/service/FreeCommentService.java b/src/main/java/org/example/tackit/domain/freeBoard/Free_comment/service/FreeCommentService.java index c22f0e2..63a4367 100644 --- a/src/main/java/org/example/tackit/domain/freeBoard/Free_comment/service/FreeCommentService.java +++ b/src/main/java/org/example/tackit/domain/freeBoard/Free_comment/service/FreeCommentService.java @@ -2,141 +2,147 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.FreeComment; +import org.example.tackit.domain.entity.FreePost; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; import org.example.tackit.domain.freeBoard.Free_comment.dto.req.FreeCommentCreateDto; import org.example.tackit.domain.freeBoard.Free_comment.dto.req.FreeCommentUpdateDto; import org.example.tackit.domain.freeBoard.Free_comment.dto.resp.FreeCommentRespDto; import org.example.tackit.domain.freeBoard.Free_comment.repository.FreeCommentRepository; import org.example.tackit.domain.freeBoard.Free_post.repository.FreePostJPARepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.notification.service.NotificationService; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.util.List; - @Service @RequiredArgsConstructor public class FreeCommentService { - private final FreeCommentRepository freeCommentRepository; - private final FreePostJPARepository freePostJPARepository; - private final MemberOrgRepository memberOrgRepository; - private final NotificationService notificationService; - - // [ 댓글 생성 ] - @Transactional - public FreeCommentRespDto createComment(FreeCommentCreateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - FreePost post = freePostJPARepository.findById(dto.getFreePostId()) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - - FreeComment comment = FreeComment.builder() - .writer(member) - .freePost(post) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .build(); - - // 1. 댓글 DB 저장 - FreeComment savedComment = freeCommentRepository.save(comment); - - // 2. 알림 전송 - if(!post.getWriter().getId().equals(member.getId())){ - MemberOrg postWriter = post.getWriter(); // 알림 받을 대상(게시글 작성자) - String message = member.getNickname() + "님이 글에 댓글을 남겼습니다."; - String url = "/api/free-posts/" + post.getId(); - - // 3. 알림 엔티티 생성 - Notification notification = Notification.builder() - .member(postWriter.getMember()) - .type(NotificationType.COMMENT) - .message(message) - .relatedUrl(url) - .fromMemberOrgId(member.getId()) - .build(); - - // 4. 알림 저장 및 전송을 위해 NotificationService 호출 - notificationService.send(notification); - } - - return new FreeCommentRespDto(savedComment); + + private final FreeCommentRepository freeCommentRepository; + private final FreePostJPARepository freePostJPARepository; + private final MemberOrgRepository memberOrgRepository; + private final NotificationService notificationService; + + // [ 댓글 생성 ] + @Transactional + public FreeCommentRespDto createComment(FreeCommentCreateDto dto, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + FreePost post = freePostJPARepository.findById(dto.getFreePostId()) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + + FreeComment comment = FreeComment.builder() + .writer(member) + .freePost(post) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .build(); + + // 1. 댓글 DB 저장 + FreeComment savedComment = freeCommentRepository.save(comment); + + // 2. 알림 전송 + if (!post.getWriter().getId().equals(member.getId())) { + MemberOrg postWriter = post.getWriter(); // 알림 받을 대상(게시글 작성자) + String message = member.getNickname() + "님이 글에 댓글을 남겼습니다."; + String url = "/api/free-posts/" + post.getId(); + + // 3. 알림 엔티티 생성 + Notification notification = Notification.builder() + .member(postWriter.getMember()) + .type(NotificationType.COMMENT) + .message(message) + .relatedUrl(url) + .fromMemberOrgId(member.getId()) + .build(); + + // 4. 알림 저장 및 전송을 위해 NotificationService 호출 + notificationService.send(notification); } - // [ 게시글 댓글 조회 ] - @Transactional - public List getCommentByPost(Long postId, Long orgId){ - FreePost post = freePostJPARepository.findById(postId) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + return new FreeCommentRespDto(savedComment); + } - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); - } + // [ 게시글 댓글 조회 ] + @Transactional + public List getCommentByPost(Long postId, Long orgId) { + FreePost post = freePostJPARepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - return freeCommentRepository.findByFreePost(post) - .stream() - .map(FreeCommentRespDto::new) - .toList(); + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); } - // [ 댓글 수정 ] : 작성자만 가능 - @Transactional - public FreeCommentRespDto updateComment(Long commentId, FreeCommentUpdateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - FreeComment comment = freeCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + return freeCommentRepository.findByFreePost(post) + .stream() + .map(FreeCommentRespDto::new) + .toList(); + } - boolean isWriter = comment.getWriter().getId().equals(member.getId()); + // [ 댓글 수정 ] : 작성자만 가능 + @Transactional + public FreeCommentRespDto updateComment(Long commentId, FreeCommentUpdateDto dto, String email, + Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if (!isWriter) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + FreeComment comment = freeCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - comment.updateContent(dto.getContent()); + boolean isWriter = comment.getWriter().getId().equals(member.getId()); - return new FreeCommentRespDto(comment); + if (!isWriter) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } - // [ 댓글 삭제 ] : 작성자, 관리자만 가능 - @Transactional - public void deleteComment(Long commentId, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + comment.updateContent(dto.getContent()); - FreeComment comment = freeCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + return new FreeCommentRespDto(comment); + } - boolean isWriter = comment.getWriter().getId().equals(member.getId()); - boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) - && (member.getMemberType() == MemberType.ADMIN); + // [ 댓글 삭제 ] : 작성자, 관리자만 가능 + @Transactional + public void deleteComment(Long commentId, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if (!isWriter && !isAdmin) { - throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); - } + FreeComment comment = freeCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - // Hard Delete - freeCommentRepository.delete(comment); + boolean isWriter = comment.getWriter().getId().equals(member.getId()); + boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) + && (member.getMemberType() == MemberType.ADMIN); + + if (!isWriter && !isAdmin) { + throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); } - // [ 댓글 신고 ] - @Transactional - public void increaseCommentReportCount(Long id, Long orgId){ - FreeComment comment = freeCommentRepository.findById(id) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + // Hard Delete + freeCommentRepository.delete(comment); + } + + // [ 댓글 신고 ] + @Transactional + public void increaseCommentReportCount(Long id, Long orgId) { + FreeComment comment = freeCommentRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - if (!comment.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); - } - comment.increaseReportCount(); + if (!comment.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); + } + comment.increaseReportCount(); - if (comment.getReportCount() >= 3) { - freeCommentRepository.delete(comment); // Hard Delete - } + if (comment.getReportCount() >= 3) { + freeCommentRepository.delete(comment); // Hard Delete } + } } diff --git a/src/main/java/org/example/tackit/domain/freeBoard/Free_post/service/FreePostService.java b/src/main/java/org/example/tackit/domain/freeBoard/Free_post/service/FreePostService.java index 4ee1fb6..58082ed 100644 --- a/src/main/java/org/example/tackit/domain/freeBoard/Free_post/service/FreePostService.java +++ b/src/main/java/org/example/tackit/domain/freeBoard/Free_post/service/FreePostService.java @@ -1,346 +1,363 @@ package org.example.tackit.domain.freeBoard.Free_post.service; +import static org.example.tackit.global.exception.ErrorCode.MEMBER_NOT_FOUND; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.example.tackit.common.dto.PageResponseDTO; import org.example.tackit.config.S3.S3UploadService; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.FreePost; +import org.example.tackit.domain.entity.FreePostImage; +import org.example.tackit.domain.entity.FreeReport; +import org.example.tackit.domain.entity.FreeScrap; +import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Post; import org.example.tackit.domain.freeBoard.Free_post.dto.request.FreePostReqDto; import org.example.tackit.domain.freeBoard.Free_post.dto.request.UpdateFreeReqDto; import org.example.tackit.domain.freeBoard.Free_post.dto.response.FreePopularPostRespDto; import org.example.tackit.domain.freeBoard.Free_post.dto.response.FreePostRespDto; import org.example.tackit.domain.freeBoard.Free_post.dto.response.FreeScrapResponseDto; -import org.example.tackit.domain.freeBoard.Free_post.repository.*; +import org.example.tackit.domain.freeBoard.Free_post.repository.FreePostImageRepository; +import org.example.tackit.domain.freeBoard.Free_post.repository.FreePostJPARepository; +import org.example.tackit.domain.freeBoard.Free_post.repository.FreePostReportRepository; +import org.example.tackit.domain.freeBoard.Free_post.repository.FreeScrapJPARepository; import org.example.tackit.domain.freeBoard.Free_tag.repository.FreePostTagMapRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.member.repository.MemberRepository; import org.example.tackit.domain.notification.service.NotificationService; -import org.example.tackit.common.dto.PageResponseDTO; -import org.example.tackit.global.exception.*; +import org.example.tackit.global.exception.AccessDeniedCustomException; +import org.example.tackit.global.exception.ErrorCode; +import org.example.tackit.global.exception.MemberNotFoundException; +import org.example.tackit.global.exception.PostInactiveException; +import org.example.tackit.global.exception.PostNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.example.tackit.global.exception.ErrorCode.MEMBER_NOT_FOUND; - @Service @RequiredArgsConstructor public class FreePostService { - private final FreePostJPARepository freePostJPARepository; - private final MemberOrgRepository memberOrgRepository; - private final MemberRepository memberRepository; - private final FreePostTagService tagService; - private final FreeScrapJPARepository freeScrapJPARepository; - private final FreePostTagMapRepository freePostTagMapRepository; - private final FreePostReportRepository freePostReportRepository; - private final S3UploadService s3UploadService; - private final FreePostImageRepository freePostImageRepository; - private final NotificationService notificationService; - - // [ 게시글 전체 조회 ] - @Transactional - public PageResponseDTO findAll(String email, Long profileId, Pageable pageable) { - // 현재 접속한 멀티프로필 정보 조회 - MemberOrg currProfile = memberOrgRepository.findByMemberEmailAndId(email, profileId) - .orElseThrow(() -> new RuntimeException("해당 조직에 대한 접근 권한이 없거나 유효하지 않은 프로필입니다.")); - - // 해당 프로필이 속한 조직의 ID 가져오기 - Long orgId = currProfile.getOrganization().getId(); - - // 해당 조직의 게시글만 조회 - Page page = freePostJPARepository.findAllByOrganizationIdAndAccountStatus( - orgId, - AccountStatus.ACTIVE, - pageable - ); - - - - return PageResponseDTO.from(page, post -> { - List tags = freePostTagMapRepository.findByFreePost(post).stream() - .map(mapping -> mapping.getTag().getTagName()) - .toList(); - - String imageUrl = post.getImages().isEmpty() ? null - : post.getImages().get(0).getImageUrl(); - - String profileImageUrl = post.getWriter().getProfileImageUrl(); - - return FreePostRespDto.builder() - .id(post.getId()) - .writer(post.isAnonymous() ? "익명" : post.getWriter().getNickname()) - .profileImageUrl(post.isAnonymous() ? null : profileImageUrl) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tags) - .imageUrl(imageUrl) - .isAnonymous(post.isAnonymous()) - .build(); - }); - } - // [ 게시글 상세 조회 ] - @Transactional - public FreePostRespDto getPostById(Long id, Long orgId, Long memberId) { - FreePost post = freePostJPARepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); + private final FreePostJPARepository freePostJPARepository; + private final MemberOrgRepository memberOrgRepository; + private final MemberRepository memberRepository; + private final FreePostTagService tagService; + private final FreeScrapJPARepository freeScrapJPARepository; + private final FreePostTagMapRepository freePostTagMapRepository; + private final FreePostReportRepository freePostReportRepository; + private final S3UploadService s3UploadService; + private final FreePostImageRepository freePostImageRepository; + private final NotificationService notificationService; + + // [ 게시글 전체 조회 ] + @Transactional + public PageResponseDTO findAll(String email, Long profileId, Pageable pageable) { + // 현재 접속한 멀티프로필 정보 조회 + MemberOrg currProfile = memberOrgRepository.findByMemberEmailAndId(email, profileId) + .orElseThrow(() -> new RuntimeException("해당 조직에 대한 접근 권한이 없거나 유효하지 않은 프로필입니다.")); + + // 해당 프로필이 속한 조직의 ID 가져오기 + Long orgId = currProfile.getOrganization().getId(); + + // 해당 조직의 게시글만 조회 + Page page = freePostJPARepository.findAllByOrganizationIdAndAccountStatus( + orgId, + AccountStatus.ACTIVE, + pageable + ); + + return PageResponseDTO.from(page, post -> { + List tags = freePostTagMapRepository.findByFreePost(post).stream() + .map(mapping -> mapping.getTag().getTagName()) + .toList(); + + String imageUrl = post.getImages().isEmpty() ? null + : post.getImages().get(0).getImageUrl(); + + String profileImageUrl = post.getWriter().getProfileImageUrl(); + + return FreePostRespDto.builder() + .id(post.getId()) + .writer(post.isAnonymous() ? "익명" : post.getWriter().getNickname()) + .profileImageUrl(post.isAnonymous() ? null : profileImageUrl) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tags) + .imageUrl(imageUrl) + .isAnonymous(post.isAnonymous()) + .build(); + }); + } + + // [ 게시글 상세 조회 ] + @Transactional + public FreePostRespDto getPostById(Long id, Long orgId, Long memberId) { + FreePost post = freePostJPARepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); + + MemberOrg currProfile = memberOrgRepository.findById(orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + if (post.getOrganization().getId().equals(currProfile.getOrganization().getId())) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); + } - MemberOrg currProfile = memberOrgRepository.findById(orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + if (!post.getAccountStatus().equals(AccountStatus.ACTIVE)) { + throw new PostInactiveException(ErrorCode.POST_IS_INACTIVE); + } - if (post.getOrganization().getId().equals(currProfile.getOrganization().getId()) ) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); - } + post.increaseViewCount(); + + List tagNames = tagService.getTagNamesByPost(post); + + String imageUrl = post.getImages().isEmpty() ? null + : post.getImages().get(0).getImageUrl(); + + String profileImageUrl = post.getWriter().getProfileImageUrl(); + + // 스크랩 여부 조회 + boolean isScrap = freeScrapJPARepository.existsByFreePostIdAndMemberId(id, memberId); + + return FreePostRespDto.builder() + .id(post.getId()) + .writer(post.isAnonymous() ? "익명" : post.getWriter().getNickname()) + .profileImageUrl(post.isAnonymous() ? null : profileImageUrl) + .title(post.getTitle()) + .content(post.getContent()) + .tags(tagNames) + .imageUrl(imageUrl) + .createdAt(post.getCreatedAt()) + .isScrap(isScrap) + .isAnonymous(post.isAnonymous()) + .build(); + } + + // [ 게시글 작성 ] + @Transactional + public FreePostRespDto createPost(FreePostReqDto dto, String email, Long profileId) + throws IOException { + + // 1. 유저 조회 + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + + // 2. Member의 Id와 Profile Id로 조회 + MemberOrg memberOrg = memberOrgRepository.findByMemberIdAndProfileId(member.getId(), profileId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + + // 2. 게시글 생성 + FreePost post = FreePost.builder() + .writer(memberOrg) + .organization(memberOrg.getOrganization()) + .title(dto.getTitle()) + .content(dto.getContent()) + .isAnonymous(dto.isAnonymous()) + .createdAt(LocalDateTime.now()) + .type(Post.Free) + .accountStatus(AccountStatus.ACTIVE) + .reportCount(0) + .build(); + + freePostJPARepository.save(post); + + // 3. 이미지 업로드 → PostImage 저장 + String imageUrl = null; + if (dto.getImage() != null && !dto.getImage().isEmpty()) { + imageUrl = s3UploadService.saveFile(dto.getImage()); + + FreePostImage image = FreePostImage.builder() + .imageUrl(imageUrl) + .freePost(post) + .build(); + + freePostImageRepository.save(image); // 따로 JPARepository 필요 + } - if (!post.getAccountStatus().equals(AccountStatus.ACTIVE)) { - throw new PostInactiveException(ErrorCode.POST_IS_INACTIVE); - } + List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); - post.increaseViewCount(); + return FreePostRespDto.builder() + .id(post.getId()) + .writer(post.isAnonymous() ? "익명" : memberOrg.getNickname()) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tagNames) + .imageUrl(imageUrl) + .isAnonymous(post.isAnonymous()) + .build(); - List tagNames = tagService.getTagNamesByPost(post); + } - String imageUrl = post.getImages().isEmpty() ? null - : post.getImages().get(0).getImageUrl(); + // [ 게시글 수정 ] : 작성자만 + @Transactional + public FreePostRespDto update(Long id, UpdateFreeReqDto req, String email, Long orgId) + throws IOException { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - String profileImageUrl = post.getWriter().getProfileImageUrl(); + FreePost post = freePostJPARepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); - // 스크랩 여부 조회 - boolean isScrap = freeScrapJPARepository.existsByFreePostIdAndMemberId(id, memberId); + boolean isWriter = post.getWriter().getId().equals(member.getId()); - return FreePostRespDto.builder() - .id(post.getId()) - .writer(post.isAnonymous() ? "익명" : post.getWriter().getNickname()) - .profileImageUrl(post.isAnonymous() ? null : profileImageUrl) - .title(post.getTitle()) - .content(post.getContent()) - .tags(tagNames) - .imageUrl(imageUrl) - .createdAt(post.getCreatedAt()) - .isScrap(isScrap) - .isAnonymous(post.isAnonymous()) - .build(); + if (!isWriter) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_EDIT); } - // [ 게시글 작성 ] - @Transactional - public FreePostRespDto createPost(FreePostReqDto dto, String email, Long profileId) throws IOException { - - // 1. 유저 조회 - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - - // 2. Member의 Id와 Profile Id로 조회 - MemberOrg memberOrg = memberOrgRepository.findByMemberIdAndProfileId(member.getId(), profileId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - - // 2. 게시글 생성 - FreePost post = FreePost.builder() - .writer(memberOrg) - .organization(memberOrg.getOrganization()) - .title(dto.getTitle()) - .content(dto.getContent()) - .isAnonymous(dto.isAnonymous()) - .createdAt(LocalDateTime.now()) - .type(Post.Free) - .accountStatus(AccountStatus.ACTIVE) - .reportCount(0) - .build(); - - freePostJPARepository.save(post); - - // 3. 이미지 업로드 → PostImage 저장 - String imageUrl = null; - if (dto.getImage() != null && !dto.getImage().isEmpty()) { - imageUrl = s3UploadService.saveFile(dto.getImage()); - - FreePostImage image = FreePostImage.builder() - .imageUrl(imageUrl) - .freePost(post) - .build(); - - freePostImageRepository.save(image); // 따로 JPARepository 필요 - } - - List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); + post.update(req.getTitle(), req.getContent()); - return FreePostRespDto.builder() - .id(post.getId()) - .writer(post.isAnonymous() ? "익명" : memberOrg.getNickname()) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tagNames) - .imageUrl(imageUrl) - .isAnonymous(post.isAnonymous()) - .build(); + tagService.deleteTagsByPost(post); + List tagNames = tagService.assignTagsToPost(post, req.getTagIds()); + String imageUrl = null; + // 1. "이미지 제거" 요청 + if (req.isRemoveImage()) { + freePostImageRepository.findByFreePostId(post.getId()) + .forEach(oldImage -> { + s3UploadService.deleteImage(oldImage.getImageUrl()); // S3 삭제 + freePostImageRepository.delete(oldImage); // DB 삭제 + }); } - // [ 게시글 수정 ] : 작성자만 - @Transactional - public FreePostRespDto update(Long id, UpdateFreeReqDto req, String email, Long orgId) throws IOException { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - - FreePost post = freePostJPARepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); - - boolean isWriter = post.getWriter().getId().equals(member.getId()); - - if (!isWriter) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_EDIT); - } - - post.update(req.getTitle(), req.getContent()); - - tagService.deleteTagsByPost(post); - List tagNames = tagService.assignTagsToPost(post, req.getTagIds()); - - String imageUrl = null; - // 1. "이미지 제거" 요청 - if (req.isRemoveImage()) { - freePostImageRepository.findByFreePostId(post.getId()) - .forEach(oldImage -> { - s3UploadService.deleteImage(oldImage.getImageUrl()); // S3 삭제 - freePostImageRepository.delete(oldImage); // DB 삭제 - }); - } - - // 2. 새 이미지 업로드 - else if (req.getImage() != null && !req.getImage().isEmpty()) { - // 기존 이미지 제거 - freePostImageRepository.findByFreePostId(post.getId()) - .forEach(oldImage -> { - s3UploadService.deleteImage(oldImage.getImageUrl()); - freePostImageRepository.delete(oldImage); - }); - - // 새 이미지 저장 - imageUrl = s3UploadService.saveFile(req.getImage()); - FreePostImage newImage = FreePostImage.builder() - .imageUrl(imageUrl) - .freePost(post) - .build(); - - freePostImageRepository.save(newImage); - } + // 2. 새 이미지 업로드 + else if (req.getImage() != null && !req.getImage().isEmpty()) { + // 기존 이미지 제거 + freePostImageRepository.findByFreePostId(post.getId()) + .forEach(oldImage -> { + s3UploadService.deleteImage(oldImage.getImageUrl()); + freePostImageRepository.delete(oldImage); + }); + + // 새 이미지 저장 + imageUrl = s3UploadService.saveFile(req.getImage()); + FreePostImage newImage = FreePostImage.builder() + .imageUrl(imageUrl) + .freePost(post) + .build(); + + freePostImageRepository.save(newImage); + } - // 3. 아무 요청 없으면 기존 이미지 유지 - else { - List images = freePostImageRepository.findByFreePostId(post.getId()); - if (!images.isEmpty()) { - imageUrl = images.get(0).getImageUrl(); - } - } + // 3. 아무 요청 없으면 기존 이미지 유지 + else { + List images = freePostImageRepository.findByFreePostId(post.getId()); + if (!images.isEmpty()) { + imageUrl = images.get(0).getImageUrl(); + } + } - return FreePostRespDto.builder() - .id(post.getId()) - .writer(post.isAnonymous() ? "익명" : member.getNickname()) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tagNames) - //.imageUrl(imageUrl) - .imageUrl(post.isAnonymous() ? null : imageUrl) - .isAnonymous(post.isAnonymous()) - .build(); + return FreePostRespDto.builder() + .id(post.getId()) + .writer(post.isAnonymous() ? "익명" : member.getNickname()) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tagNames) + //.imageUrl(imageUrl) + .imageUrl(post.isAnonymous() ? null : imageUrl) + .isAnonymous(post.isAnonymous()) + .build(); + } + + // [ 게시글 삭제 ] : 작성자, 관리자만 + @Transactional + public void delete(Long id, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + + FreePost post = freePostJPARepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); + + boolean isWriter = post.getWriter().getId().equals(member.getId()); + boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) + && (member.getMemberType() == MemberType.ADMIN); + + if (!isAdmin && !isWriter) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_DELETE); } - // [ 게시글 삭제 ] : 작성자, 관리자만 - @Transactional - public void delete(Long id, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + post.delete(); // Soft Deleted + } - FreePost post = freePostJPARepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); + // [ 게시글 신고 ] + @Transactional + public String report(Long postId, Long orgId) { + MemberOrg member = memberOrgRepository.findById(orgId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - boolean isWriter = post.getWriter().getId().equals(member.getId()); - boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) - && (member.getMemberType() == MemberType.ADMIN); + FreePost post = freePostJPARepository.findById(postId) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); - if (!isAdmin && !isWriter) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_DELETE); - } + boolean alreadyReported = freePostReportRepository.existsByMemberAndFreePost(member, post); - post.delete(); // Soft Deleted + if (alreadyReported) { + return "이미 신고한 게시글입니다."; } - - // [ 게시글 신고 ] - @Transactional - public String report(Long postId, Long orgId) { - MemberOrg member = memberOrgRepository.findById(orgId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - - FreePost post = freePostJPARepository.findById(postId) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); - - boolean alreadyReported = freePostReportRepository.existsByMemberAndFreePost(member, post); - - if (alreadyReported) { - return "이미 신고한 게시글입니다."; - } - freePostReportRepository.save( - FreeReport.builder() - .member(member) - .freePost(post) - .build() - ); - // 신고 횟수 증가 - post.increaseReportCount(); - return "게시글을 신고하였습니다."; + freePostReportRepository.save( + FreeReport.builder() + .member(member) + .freePost(post) + .build() + ); + // 신고 횟수 증가 + post.increaseReportCount(); + return "게시글을 신고하였습니다."; + } + + + // [ 게시글 스크랩 ] + @Transactional + public FreeScrapResponseDto toggleScrap(Long postId, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + + FreePost post = freePostJPARepository.findById(postId) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); + + if (!post.getOrganization().equals(orgId)) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); } + Optional existing = freeScrapJPARepository.findByMemberAndFreePost(member, post); - // [ 게시글 스크랩 ] - @Transactional - public FreeScrapResponseDto toggleScrap(Long postId, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); - - FreePost post = freePostJPARepository.findById(postId) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); - - if (!post.getOrganization().equals(orgId)) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); - } - - Optional existing = freeScrapJPARepository.findByMemberAndFreePost(member, post); - - if (existing.isPresent()) { - freeScrapJPARepository.delete(existing.get()); - post.decreaseScrapCount(); - return new FreeScrapResponseDto(false, null); - } + if (existing.isPresent()) { + freeScrapJPARepository.delete(existing.get()); + post.decreaseScrapCount(); + return new FreeScrapResponseDto(false, null); + } - FreeScrap scrap = FreeScrap.builder() - .member(member) - .freePost(post) - .savedAt(LocalDateTime.now()) - .build(); - - freeScrapJPARepository.save(scrap); - post.increaseScrapCount(); - - // 알림 전송 - if( !post.getWriter().getId().equals(member.getId())) { - notificationService.send(Notification.builder() - .member(post.getWriter().getMember()) - .memberOrgId(post.getWriter().getId()) - .type(NotificationType.SCRAP) - .message(member.getNickname() + "님이 글을 스크랩하였습니다.") - .fromMemberOrgId(member.getId()) - .relatedUrl("/api/free-posts/" + post.getId()) - .build()); - } - return new FreeScrapResponseDto(true, scrap.getSavedAt()); + FreeScrap scrap = FreeScrap.builder() + .member(member) + .freePost(post) + .savedAt(LocalDateTime.now()) + .build(); + + freeScrapJPARepository.save(scrap); + post.increaseScrapCount(); + + // 알림 전송 + if (!post.getWriter().getId().equals(member.getId())) { + notificationService.send(Notification.builder() + .member(post.getWriter().getMember()) + .memberOrgId(post.getWriter().getId()) + .type(NotificationType.SCRAP) + .message(member.getNickname() + "님이 글을 스크랩하였습니다.") + .fromMemberOrgId(member.getId()) + .relatedUrl("/api/free-posts/" + post.getId()) + .build()); + } + return new FreeScrapResponseDto(true, scrap.getSavedAt()); /* if(!post.getWriter().getId().equals(member.getId())){ Member postWriter = post.getWriter(); @@ -362,22 +379,23 @@ public FreeScrapResponseDto toggleScrap(Long postId, String email, Long orgId) { return new FreeScrapResponseDto(true, scrap.getSavedAt()); */ - } - - /* - public List getPopularPosts(Long orgId) { - return freePostJPARepository.findTop3ByWriterIdAndAccountStatusOrderByViewCountDescScrapCountDesc(orgId, AccountStatus.ACTIVE) - .stream() - .map(FreePopularPostRespDto::from) - .toList(); - */ - // 인기 3개 - @Transactional(readOnly = true) - public List getPopularPosts(Long orgId) { - return freePostJPARepository.findTop3ByOrganizationIdAndAccountStatusOrderByViewCountDescScrapCountDesc(orgId, AccountStatus.ACTIVE) - .stream() - .map(FreePopularPostRespDto::from) - .toList(); + } + + /* + public List getPopularPosts(Long orgId) { + return freePostJPARepository.findTop3ByWriterIdAndAccountStatusOrderByViewCountDescScrapCountDesc(orgId, AccountStatus.ACTIVE) + .stream() + .map(FreePopularPostRespDto::from) + .toList(); + */ + // 인기 3개 + @Transactional(readOnly = true) + public List getPopularPosts(Long orgId) { + return freePostJPARepository.findTop3ByOrganizationIdAndAccountStatusOrderByViewCountDescScrapCountDesc( + orgId, AccountStatus.ACTIVE) + .stream() + .map(FreePopularPostRespDto::from) + .toList(); /* .filter(post -> post.getWriter().getOrganization().equals(organization)) .sorted(Comparator @@ -394,6 +412,6 @@ public List getPopularPosts(Long orgId) { .map(FreePopularPostRespDto::from) .toList(); */ - } + } } diff --git a/src/main/java/org/example/tackit/domain/member/component/MemberOrgValidator.java b/src/main/java/org/example/tackit/domain/member/component/MemberOrgValidator.java new file mode 100644 index 0000000..1ef3d16 --- /dev/null +++ b/src/main/java/org/example/tackit/domain/member/component/MemberOrgValidator.java @@ -0,0 +1,41 @@ +package org.example.tackit.domain.member.component; + +import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Org.OrgStatus; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberOrgValidator { + + private final MemberOrgRepository memberOrgRepository; + + // 활동 회원 체크 메서드 + public MemberOrg validateActiveMembership(Long orgId, Long memberOrgId) { + MemberOrg memberOrg = memberOrgRepository.findById(memberOrgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 조직 멤버 프로필입니다.")); + + if (!memberOrg.getOrganization().getId().equals(orgId)) { + throw new IllegalArgumentException("해당 조직의 프로필이 아닙니다."); + } + + if (memberOrg.getOrgStatus() != OrgStatus.ACTIVE) { + throw new IllegalArgumentException("해당 조직의 활동 중인 회원만 접근할 수 있습니다."); + } + return memberOrg; + } + + public MemberOrg validateExecutive(Long orgId, Long memberOrgId) { + MemberOrg memberOrg = validateActiveMembership(orgId, memberOrgId); + + if (memberOrg.getMemberRole() != MemberRole.EXECUTIVE) { + throw new IllegalArgumentException("해당 조직의 운영진만 접근할 수 있습니다."); + } + return memberOrg; + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/member/dto/SimpleMemberProfileDto.java b/src/main/java/org/example/tackit/domain/member/dto/SimpleMemberProfileDto.java new file mode 100644 index 0000000..91567ad --- /dev/null +++ b/src/main/java/org/example/tackit/domain/member/dto/SimpleMemberProfileDto.java @@ -0,0 +1,25 @@ +package org.example.tackit.domain.member.dto; + +import lombok.Builder; +import lombok.Getter; +import org.example.tackit.domain.entity.Org.MemberOrg; + +@Getter +@Builder +public class SimpleMemberProfileDto { + + private Long orgMemberId; + private String nickname; + private String profileImageUrl; + + public static SimpleMemberProfileDto from(MemberOrg memberOrg) { + if (memberOrg == null) { + return null; + } + return SimpleMemberProfileDto.builder() + .orgMemberId(memberOrg.getId()) + .nickname(memberOrg.getNickname()) + .profileImageUrl(memberOrg.getProfileImageUrl()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/member/repository/MemberOrgRepository.java b/src/main/java/org/example/tackit/domain/member/repository/MemberOrgRepository.java new file mode 100644 index 0000000..2a0d00e --- /dev/null +++ b/src/main/java/org/example/tackit/domain/member/repository/MemberOrgRepository.java @@ -0,0 +1,55 @@ +package org.example.tackit.domain.member.repository; + +import java.util.List; +import java.util.Optional; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Org.OrgStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberOrgRepository extends JpaRepository { + + // 특정 소속(Org) 내에서 닉네임 중복 확인 + boolean existsByOrganizationIdAndNickname(Long orgId, String nickname); + + // 해당 소속(Org)에 가입된 사람이 있는지 확인 (최초 가입자 판별용) + boolean existsByOrganizationId(Long orgId); + + @Query("SELECT mo FROM MemberOrg mo WHERE mo.member.id = :memberId AND mo.id = :profileId") + Optional findByMemberIdAndProfileId(@Param("memberId") Long memberId, + @Param("profileId") Long profileId); + + // 멀티 프로필 : 사용자의 이메일로 가입된 모든 프로필 조회 + List findAllByMemberEmail(String email); + + // 사용자의 ID로 가입된 모든 프로필 조회 + List findAllByMemberId(Long memberId); + + // 이메일과 프로필(MemberOrg) ID로 특정 소속 정보 조회 + Optional findByMemberEmailAndId(String email, Long id); + + Optional findByMemberIdAndId(Long memberId, Long profileId); + + // 특정 유저가 해당 프로필의 소유자인지 확인 + boolean existsByMemberIdAndId(Long memberId, Long id); + + // N+1 문제 방지를 위한 페치 조인 (성능 최적화) + @Query("SELECT mo FROM MemberOrg mo JOIN FETCH mo.member WHERE mo.id = :id") + Optional findByIdWithMember(@Param("id") Long id); + + // 조직 ID와 멤버 ID로 가입 여부 확인 (중복 가입 방지용) + boolean existsByMemberIdAndOrganizationId(Long memberId, Long orgId); + + Optional findByMemberIdAndOrganizationId(Long memberId, Long orgId); + + boolean existsByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, + OrgStatus status); + + Optional findByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, + OrgStatus status); + + int countByOrganizationIdAndOrgStatus(Long orgId, OrgStatus orgStatus); +} \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/member/repository/MemberRepository.java b/src/main/java/org/example/tackit/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..6f9102b --- /dev/null +++ b/src/main/java/org/example/tackit/domain/member/repository/MemberRepository.java @@ -0,0 +1,27 @@ +package org.example.tackit.domain.member.repository; + +import java.util.Optional; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); //그 유저 실제 정보 추출 + + boolean existsByEmail(String email); // 이메일 존재 확인 + + // boolean existsByNickname(String nickname); //닉네임 중복 확인 + boolean existsByEmailAndStatus(String email, AccountStatus accountStatus); // 이메일+상태 존재 확인 + + Optional findByEmailAndStatus(String email, AccountStatus accountStatus); // 이메일+상태 정보 추출 + + // Optional findByOrganizationAndName(String organization, String name); + + // Optional findByEmailAndOrganization(String email, String organization); + + // Optional findByNameAndOrganizationAndEmail(String name, String organization, String email); +} + diff --git a/src/main/java/org/example/tackit/domain/mypage/service/MyPageQnAService.java b/src/main/java/org/example/tackit/domain/mypage/service/MyPageQnAService.java index d94ab07..10910ab 100644 --- a/src/main/java/org/example/tackit/domain/mypage/service/MyPageQnAService.java +++ b/src/main/java/org/example/tackit/domain/mypage/service/MyPageQnAService.java @@ -1,65 +1,69 @@ package org.example.tackit.domain.mypage.service; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.qnaBoard.QnA_comment.repository.QnACommentRepository; -import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAPostRepository; -import org.example.tackit.domain.qnaBoard.QnA_tag.repository.QnAPostTagMapRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; +import org.example.tackit.common.dto.PageResponseDTO; +import org.example.tackit.domain.entity.AccountStatus; import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.QnAComment; +import org.example.tackit.domain.entity.QnAPost; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.mypage.dto.response.QnAMyCommentResponseDto; import org.example.tackit.domain.mypage.dto.response.QnAMyPostResponseDto; -import org.example.tackit.common.dto.PageResponseDTO; +import org.example.tackit.domain.qnaBoard.QnA_comment.repository.QnACommentRepository; +import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAPostRepository; +import org.example.tackit.domain.qnaBoard.QnA_tag.repository.QnAPostTagMapRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class MyPageQnAService { - private final MemberOrgRepository memberOrgRepository; - private final QnAPostRepository qnAPostRepository; - private final QnACommentRepository qnACommentRepository; - private final QnAPostTagMapRepository qnAPostTagMapRepository; - // 내가 쓴 게시글 조회 - @Transactional(readOnly = true) - public PageResponseDTO getMyPosts(String email, Long orgId, Pageable pageable) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + private final MemberOrgRepository memberOrgRepository; + private final QnAPostRepository qnAPostRepository; + private final QnACommentRepository qnACommentRepository; + private final QnAPostTagMapRepository qnAPostTagMapRepository; - Page postPage = qnAPostRepository.findByWriterAndAccountStatus(member, AccountStatus.ACTIVE, pageable); - List posts = postPage.getContent(); + // 내가 쓴 게시글 조회 + @Transactional(readOnly = true) + public PageResponseDTO getMyPosts(String email, Long orgId, + Pageable pageable) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - // 태그 일괄 조회 (N+1 방지) - Map> tagMap = qnAPostTagMapRepository.findByQnaPostIn(posts).stream() - .collect(Collectors.groupingBy( - mapping -> mapping.getQnaPost().getId(), - Collectors.mapping(mapping -> mapping.getTag().getTagName(), Collectors.toList()) - )); + Page postPage = qnAPostRepository.findByWriterAndAccountStatus(member, + AccountStatus.ACTIVE, pageable); + List posts = postPage.getContent(); - return PageResponseDTO.from(postPage, post -> - QnAMyPostResponseDto.fromEntity(post, tagMap.getOrDefault(post.getId(), List.of())) - ); - } + // 태그 일괄 조회 (N+1 방지) + Map> tagMap = qnAPostTagMapRepository.findByQnaPostIn(posts).stream() + .collect(Collectors.groupingBy( + mapping -> mapping.getQnaPost().getId(), + Collectors.mapping(mapping -> mapping.getTag().getTagName(), Collectors.toList()) + )); + return PageResponseDTO.from(postPage, post -> + QnAMyPostResponseDto.fromEntity(post, tagMap.getOrDefault(post.getId(), List.of())) + ); + } - // 내가 쓴 댓글 조회 - @Transactional(readOnly = true) - public PageResponseDTO getMyComments(String email, Long orgId, Pageable pageable) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - Page commentPage = qnACommentRepository.findByWriter(member, pageable); + // 내가 쓴 댓글 조회 + @Transactional(readOnly = true) + public PageResponseDTO getMyComments(String email, Long orgId, + Pageable pageable) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - return PageResponseDTO.from(commentPage, QnAMyCommentResponseDto::fromEntity); - } + Page commentPage = qnACommentRepository.findByWriter(member, pageable); + return PageResponseDTO.from(commentPage, QnAMyCommentResponseDto::fromEntity); + } } diff --git a/src/main/java/org/example/tackit/domain/mypage/service/MyPageTipService.java b/src/main/java/org/example/tackit/domain/mypage/service/MyPageTipService.java index 6cd30fe..cfa846f 100644 --- a/src/main/java/org/example/tackit/domain/mypage/service/MyPageTipService.java +++ b/src/main/java/org/example/tackit/domain/mypage/service/MyPageTipService.java @@ -1,33 +1,24 @@ package org.example.tackit.domain.mypage.service; import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.admin.repository.AdminMemberRepository; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.tipBoard.Tip_comment.repository.TipCommentRepository; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipPostRepository; -import org.example.tackit.domain.tipBoard.Tip_tag.repository.TipPostTagMapRepository; -import org.example.tackit.domain.admin.repository.AdminMemberRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.mypage.dto.response.TipMyCommentResponseDto; -import org.example.tackit.domain.mypage.dto.response.TipMyPostResponseDto; -import org.example.tackit.domain.mypage.dto.response.TipScrapResponse; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipScrapRepository; -import org.example.tackit.common.dto.PageResponseDTO; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.example.tackit.domain.tipBoard.Tip_tag.repository.TipPostTagMapRepository; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; @Service @RequiredArgsConstructor public class MyPageTipService { - private final TipScrapRepository tipScrapRepository; - private final AdminMemberRepository adminMemberRepository; - private final TipPostRepository tipPostRepository; - private final TipPostTagMapRepository tipPostTagMapRepository; - private final TipCommentRepository tipCommentRepository; - private final MemberOrgRepository memberOrgRepository; + + private final TipScrapRepository tipScrapRepository; + private final AdminMemberRepository adminMemberRepository; + private final TipPostRepository tipPostRepository; + private final TipPostTagMapRepository tipPostTagMapRepository; + private final TipCommentRepository tipCommentRepository; + private final MemberOrgRepository memberOrgRepository; /* // 스크랩한 tip 게시글 조회 diff --git a/src/main/java/org/example/tackit/domain/noticeBoard/Notice_comment/service/NoticeCommentService.java b/src/main/java/org/example/tackit/domain/noticeBoard/Notice_comment/service/NoticeCommentService.java index db43568..e34a175 100644 --- a/src/main/java/org/example/tackit/domain/noticeBoard/Notice_comment/service/NoticeCommentService.java +++ b/src/main/java/org/example/tackit/domain/noticeBoard/Notice_comment/service/NoticeCommentService.java @@ -2,146 +2,152 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.NoticeComment; +import org.example.tackit.domain.entity.NoticePost; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.member.repository.MemberRepository; import org.example.tackit.domain.noticeBoard.Notice_comment.dto.req.NoticeCommentCreateDto; import org.example.tackit.domain.noticeBoard.Notice_comment.dto.req.NoticeCommentUpdateDto; import org.example.tackit.domain.noticeBoard.Notice_comment.dto.resp.NoticeCommentRespDto; import org.example.tackit.domain.noticeBoard.Notice_comment.repository.NoticeCommentRepository; import org.example.tackit.domain.noticeBoard.Notice_post.repository.NoticePostRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; import org.example.tackit.domain.notification.service.NotificationService; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; -import java.util.List; - @Service @RequiredArgsConstructor public class NoticeCommentService { - private final NoticeCommentRepository noticeCommentRepository; - private final NoticePostRepository noticePostRepository; - private final MemberRepository memberRepository; - private final NotificationService notificationService; - private final MemberOrgRepository memberOrgRepository; - - // [ 댓글 생성 ] - @Transactional - public NoticeCommentRespDto createComment(NoticeCommentCreateDto dto, String email, Long orgId){ - // 현재 접속한 소속의 프로필 조회 - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - NoticePost post = noticePostRepository.findById(dto.getNoticePostId()) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - - // 댓글 엔티티 생성 - NoticeComment comment = NoticeComment.builder() - .writer(memberProfile) - .noticePost(post) - .content(dto.getContent()) - .build(); - - NoticeComment savedComment = noticeCommentRepository.save(comment); - - // 알림 전송 - if(!post.getWriter().getId().equals(memberProfile.getId())){ - // 알림 받을 대상 - MemberOrg postWriterProfile = post.getWriter(); - - String message = memberProfile.getNickname() + "님이 글에 댓글을 남겼습니다."; - String url = "/api/notice-posts/" + post.getId(); - - // 3. 알림 엔티티 생성 - Notification notification = Notification.builder() - .member(postWriterProfile.getMember()) - .memberOrgId(postWriterProfile.getId()) - .type(NotificationType.COMMENT) - .message(message) - .relatedUrl(url) - .fromMemberOrgId(memberProfile.getId()) - .build(); - - // 4. 알림 저장 및 전송을 위해 NotificationService 호출 - notificationService.send(notification); - } - - return new NoticeCommentRespDto(savedComment); + + private final NoticeCommentRepository noticeCommentRepository; + private final NoticePostRepository noticePostRepository; + private final MemberRepository memberRepository; + private final NotificationService notificationService; + private final MemberOrgRepository memberOrgRepository; + + // [ 댓글 생성 ] + @Transactional + public NoticeCommentRespDto createComment(NoticeCommentCreateDto dto, String email, Long orgId) { + // 현재 접속한 소속의 프로필 조회 + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + NoticePost post = noticePostRepository.findById(dto.getNoticePostId()) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + + // 댓글 엔티티 생성 + NoticeComment comment = NoticeComment.builder() + .writer(memberProfile) + .noticePost(post) + .content(dto.getContent()) + .build(); + + NoticeComment savedComment = noticeCommentRepository.save(comment); + + // 알림 전송 + if (!post.getWriter().getId().equals(memberProfile.getId())) { + // 알림 받을 대상 + MemberOrg postWriterProfile = post.getWriter(); + + String message = memberProfile.getNickname() + "님이 글에 댓글을 남겼습니다."; + String url = "/api/notice-posts/" + post.getId(); + + // 3. 알림 엔티티 생성 + Notification notification = Notification.builder() + .member(postWriterProfile.getMember()) + .memberOrgId(postWriterProfile.getId()) + .type(NotificationType.COMMENT) + .message(message) + .relatedUrl(url) + .fromMemberOrgId(memberProfile.getId()) + .build(); + + // 4. 알림 저장 및 전송을 위해 NotificationService 호출 + notificationService.send(notification); } - // [ 게시글 댓글 조회 ] - @Transactional - public List getCommentByPost(Long postId, Long orgId){ - NoticePost post = noticePostRepository.findById(postId) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + return new NoticeCommentRespDto(savedComment); + } - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); - } + // [ 게시글 댓글 조회 ] + @Transactional + public List getCommentByPost(Long postId, Long orgId) { + NoticePost post = noticePostRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - return noticeCommentRepository.findByNoticePost(post) - .stream() - .map(NoticeCommentRespDto::new) - .toList(); + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); } - // [ 댓글 수정 ] : 작성자만 가능 - @Transactional - public NoticeCommentRespDto updateComment(Long commentId, NoticeCommentUpdateDto dto, String email, Long orgId){ - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - NoticeComment comment = noticeCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + return noticeCommentRepository.findByNoticePost(post) + .stream() + .map(NoticeCommentRespDto::new) + .toList(); + } - if (!comment.getWriter().getId().equals(memberProfile.getId())) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + // [ 댓글 수정 ] : 작성자만 가능 + @Transactional + public NoticeCommentRespDto updateComment(Long commentId, NoticeCommentUpdateDto dto, + String email, Long orgId) { + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - comment.updateContent(dto.getContent()); + NoticeComment comment = noticeCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - return new NoticeCommentRespDto(comment); + if (!comment.getWriter().getId().equals(memberProfile.getId())) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } - // [ 댓글 삭제 ] : 작성자, 관리자만 가능 - @Transactional - public void deleteComment(Long commentId, String email, Long orgId){ - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + comment.updateContent(dto.getContent()); + + return new NoticeCommentRespDto(comment); + } - NoticeComment comment = noticeCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + // [ 댓글 삭제 ] : 작성자, 관리자만 가능 + @Transactional + public void deleteComment(Long commentId, String email, Long orgId) { + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - boolean isWriter = comment.getWriter().getId().equals(memberProfile.getId()); - boolean isAdmin = (memberProfile.getMemberRole() == MemberRole.ADMIN) - && (memberProfile.getMemberType() == MemberType.ADMIN); + NoticeComment comment = noticeCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - if (!isWriter && !isAdmin) { - throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); - } + boolean isWriter = comment.getWriter().getId().equals(memberProfile.getId()); + boolean isAdmin = (memberProfile.getMemberRole() == MemberRole.ADMIN) + && (memberProfile.getMemberType() == MemberType.ADMIN); - // Hard Delete - noticeCommentRepository.delete(comment); + if (!isWriter && !isAdmin) { + throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); } - // [ 댓글 신고 ] - @Transactional - public void increaseCommentReportCount(long commmentId, Long orgId){ + // Hard Delete + noticeCommentRepository.delete(comment); + } - NoticeComment comment = noticeCommentRepository.findById(commmentId) - .orElseThrow( () -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다.")); + // [ 댓글 신고 ] + @Transactional + public void increaseCommentReportCount(long commmentId, Long orgId) { - if (!comment.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); - } - comment.increaseReportCount(); + NoticeComment comment = noticeCommentRepository.findById(commmentId) + .orElseThrow(() -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다.")); + + if (!comment.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); + } + comment.increaseReportCount(); - if (comment.getReportCount() >= 3) { - noticeCommentRepository.delete(comment); // Hard Delete - } + if (comment.getReportCount() >= 3) { + noticeCommentRepository.delete(comment); // Hard Delete } + } } diff --git a/src/main/java/org/example/tackit/domain/noticeBoard/Notice_post/service/NoticePostService.java b/src/main/java/org/example/tackit/domain/noticeBoard/Notice_post/service/NoticePostService.java index a6268ca..6b953c0 100644 --- a/src/main/java/org/example/tackit/domain/noticeBoard/Notice_post/service/NoticePostService.java +++ b/src/main/java/org/example/tackit/domain/noticeBoard/Notice_post/service/NoticePostService.java @@ -1,8 +1,22 @@ package org.example.tackit.domain.noticeBoard.Notice_post.service; +import static org.example.tackit.global.exception.ErrorCode.MEMBER_NOT_FOUND; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.example.tackit.common.dto.PageResponseDTO; import org.example.tackit.config.S3.S3UploadService; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.NoticePost; +import org.example.tackit.domain.entity.NoticePostImage; +import org.example.tackit.domain.entity.NoticeScrap; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Post; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.noticeBoard.Notice_post.dto.request.NoticePostReqDto; import org.example.tackit.domain.noticeBoard.Notice_post.dto.request.UpdateNoticeReqDto; import org.example.tackit.domain.noticeBoard.Notice_post.dto.response.NoticePostRespDto; @@ -10,258 +24,256 @@ import org.example.tackit.domain.noticeBoard.Notice_post.repository.NoticePostImageRepository; import org.example.tackit.domain.noticeBoard.Notice_post.repository.NoticePostRepository; import org.example.tackit.domain.noticeBoard.Notice_post.repository.NoticeScrapRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; import org.example.tackit.domain.notification.service.NotificationService; -import org.example.tackit.global.exception.*; +import org.example.tackit.global.exception.AccessDeniedCustomException; +import org.example.tackit.global.exception.ErrorCode; +import org.example.tackit.global.exception.MemberNotFoundException; +import org.example.tackit.global.exception.PostNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.example.tackit.global.exception.ErrorCode.MEMBER_NOT_FOUND; - @Service @RequiredArgsConstructor public class NoticePostService { - private final NoticePostRepository noticePostRepository; - private final NoticeScrapRepository noticeScrapRepository; - private final S3UploadService s3UploadService; - private final NoticePostImageRepository noticePostImageRepository; - private final NotificationService notificationService; - private final MemberOrgRepository memberOrgRepository; - - // [ 게시글 전체 조회 ] - @Transactional - public PageResponseDTO findAll(Long orgId, Pageable pageable ) { - - Page page = noticePostRepository.findByWriterId(orgId, pageable); - return PageResponseDTO.from(page, post -> { - String imageUrl = post.getImages().isEmpty() ? null - : post.getImages().get(0).getImageUrl(); - - return NoticePostRespDto.builder() - .id(post.getId()) - .writer(post.getWriter().getNickname()) - .title(post.getTitle()) - .content(post.getContent()) - .imageUrl(imageUrl) - .createdAt(post.getCreatedAt()) - .commentEnabled(post.isCommentEnabled()) - .build(); - }); - } - // [ 게시글 상세 조회 ] - @Transactional - public NoticePostRespDto getPostById(Long id, Long orgId, Long memberId) { - NoticePost post = noticePostRepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); - - // 해당 게시글이 현재 접속한 소속의 글인지 검증 - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); - } - post.increaseViewCount(); - - String imageUrl = post.getImages().isEmpty() ? null - : post.getImages().get(0).getImageUrl(); - - String profileImageUrl = post.getWriter().getProfileImageUrl(); - - // 스크랩 여부 조회 - boolean isScrap = noticeScrapRepository.existsByNoticePostIdAndMemberId(id, memberId); - - return NoticePostRespDto.builder() - .id(post.getId()) - .writer(post.getWriter().getNickname()) - .profileImageUrl(post.getWriter().getProfileImageUrl()) - .title(post.getTitle()) - .content(post.getContent()) - .imageUrl(imageUrl) - .createdAt(post.getCreatedAt()) - .isScrap(isScrap) - .commentEnabled(post.isCommentEnabled()) - .build(); + private final NoticePostRepository noticePostRepository; + private final NoticeScrapRepository noticeScrapRepository; + private final S3UploadService s3UploadService; + private final NoticePostImageRepository noticePostImageRepository; + private final NotificationService notificationService; + private final MemberOrgRepository memberOrgRepository; + + // [ 게시글 전체 조회 ] + @Transactional + public PageResponseDTO findAll(Long orgId, Pageable pageable) { + + Page page = noticePostRepository.findByWriterId(orgId, pageable); + return PageResponseDTO.from(page, post -> { + String imageUrl = post.getImages().isEmpty() ? null + : post.getImages().get(0).getImageUrl(); + + return NoticePostRespDto.builder() + .id(post.getId()) + .writer(post.getWriter().getNickname()) + .title(post.getTitle()) + .content(post.getContent()) + .imageUrl(imageUrl) + .createdAt(post.getCreatedAt()) + .commentEnabled(post.isCommentEnabled()) + .build(); + }); + } + + // [ 게시글 상세 조회 ] + @Transactional + public NoticePostRespDto getPostById(Long id, Long orgId, Long memberId) { + NoticePost post = noticePostRepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); + + // 해당 게시글이 현재 접속한 소속의 글인지 검증 + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); + } + post.increaseViewCount(); + + String imageUrl = post.getImages().isEmpty() ? null + : post.getImages().get(0).getImageUrl(); + + String profileImageUrl = post.getWriter().getProfileImageUrl(); + + // 스크랩 여부 조회 + boolean isScrap = noticeScrapRepository.existsByNoticePostIdAndMemberId(id, memberId); + + return NoticePostRespDto.builder() + .id(post.getId()) + .writer(post.getWriter().getNickname()) + .profileImageUrl(post.getWriter().getProfileImageUrl()) + .title(post.getTitle()) + .content(post.getContent()) + .imageUrl(imageUrl) + .createdAt(post.getCreatedAt()) + .isScrap(isScrap) + .commentEnabled(post.isCommentEnabled()) + .build(); + } + + // [ 게시글 작성 ] + @Transactional + public NoticePostRespDto createPost(NoticePostReqDto dto, MultipartFile image, String email, + Long orgId) throws IOException { + + // 현재 접속한 프로필 조회 + MemberOrg writerProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + + // 권한 확인 = 동아리 운영진만 작성 가능 + if (writerProfile.getMemberRole() != MemberRole.EXECUTIVE) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_NOTICE); } - // [ 게시글 작성 ] - @Transactional - public NoticePostRespDto createPost(NoticePostReqDto dto, MultipartFile image, String email, Long orgId) throws IOException { + // 2. 게시글 생성 + NoticePost post = NoticePost.builder() + .writer(writerProfile) + .title(dto.getTitle()) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .type(Post.Notice) + .commentEnabled(dto.isCommentEnabled()) + .build(); + + noticePostRepository.save(post); + + // 3. 이미지 업로드 -> 인자로 받은 image 직접 사용 + String imageUrl = null; + if (image != null && !image.isEmpty()) { + imageUrl = s3UploadService.saveFile(image); + + NoticePostImage postImage = NoticePostImage.builder() + .imageUrl(imageUrl) + .noticePost(post) + .build(); + + noticePostImageRepository.save(postImage); + } - // 현재 접속한 프로필 조회 - MemberOrg writerProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND)); + return NoticePostRespDto.from(post, imageUrl); - // 권한 확인 = 동아리 운영진만 작성 가능 - if (writerProfile.getMemberRole() != MemberRole.EXECUTIVE) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_NOTICE); - } + } - // 2. 게시글 생성 - NoticePost post = NoticePost.builder() - .writer(writerProfile) - .title(dto.getTitle()) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .type(Post.Notice) - .commentEnabled(dto.isCommentEnabled()) - .build(); + // [ 게시글 수정 ] : 작성자만 + @Transactional + public NoticePostRespDto update(Long id, UpdateNoticeReqDto req, MultipartFile image, + String email, Long orgId) throws IOException { - noticePostRepository.save(post); + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - // 3. 이미지 업로드 -> 인자로 받은 image 직접 사용 - String imageUrl = null; - if(image != null && !image.isEmpty()) { - imageUrl = s3UploadService.saveFile(image); + NoticePost post = noticePostRepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); - NoticePostImage postImage = NoticePostImage.builder() - .imageUrl(imageUrl) - .noticePost(post) - .build(); + // 작성자 본인(프로필 ID 기준)인지 확인 + if (!post.getWriter().getId().equals(memberProfile.getId())) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_EDIT); + } - noticePostImageRepository.save(postImage); - } + post.update(req.getTitle(), req.getContent(), req.isCommentEnabled()); - return NoticePostRespDto.from(post, imageUrl); + String imageUrl = null; + // 1. "이미지 제거" 요청 + if (req.isRemoveImage()) { + deleteExistingImages(post); + imageUrl = null; } - // [ 게시글 수정 ] : 작성자만 - @Transactional - public NoticePostRespDto update(Long id, UpdateNoticeReqDto req, MultipartFile image, String email, Long orgId) throws IOException { + // 2. 새 이미지 업로드 + else if (image != null && !image.isEmpty()) { + // 기존 이미지 제거 + deleteExistingImages(post); + imageUrl = s3UploadService.saveFile(image); - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + NoticePostImage newImage = NoticePostImage.builder() + .imageUrl(imageUrl) + .noticePost(post) + .build(); - NoticePost post = noticePostRepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); + noticePostImageRepository.save(newImage); + } - // 작성자 본인(프로필 ID 기준)인지 확인 - if (!post.getWriter().getId().equals(memberProfile.getId())) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_EDIT); - } + // 3. 아무 요청 없으면 기존 이미지 유지 + else { + imageUrl = noticePostImageRepository.findByNoticePostId(post.getId()) + .stream() + .findFirst() + .map(NoticePostImage::getImageUrl) + .orElse(null); + } - post.update(req.getTitle(), req.getContent(), req.isCommentEnabled()); + return NoticePostRespDto.from(post, imageUrl); + } - String imageUrl = null; + // [ 게시글 삭제 ] : 작성자, 관리자만 + @Transactional + public void delete(Long id, String email, Long orgId) { + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - // 1. "이미지 제거" 요청 - if (req.isRemoveImage()) { - deleteExistingImages(post); - imageUrl = null; - } + NoticePost post = noticePostRepository.findById(id) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); - // 2. 새 이미지 업로드 - else if (image != null && !image.isEmpty()) { - // 기존 이미지 제거 - deleteExistingImages(post); - imageUrl = s3UploadService.saveFile(image); + boolean isWriter = post.getWriter().getId().equals(memberProfile.getId()); + boolean isAdmin = (memberProfile.getMemberRole() == MemberRole.ADMIN); - NoticePostImage newImage = NoticePostImage.builder() - .imageUrl(imageUrl) - .noticePost(post) - .build(); + if (!isAdmin && !isWriter) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_DELETE); + } - noticePostImageRepository.save(newImage); - } + noticePostRepository.delete(post); + } - // 3. 아무 요청 없으면 기존 이미지 유지 - else { - imageUrl = noticePostImageRepository.findByNoticePostId(post.getId()) - .stream() - .findFirst() - .map(NoticePostImage::getImageUrl) - .orElse(null); - } - return NoticePostRespDto.from(post, imageUrl); + // [ 게시글 스크랩 ] + @Transactional + public NoticeScrapRespDto toggleScrap(Long postId, String email, Long orgId) { + MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + NoticePost post = noticePostRepository.findById(postId) + .orElseThrow(() -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND)); + + // 같은 소속인지 체크 + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); } - // [ 게시글 삭제 ] : 작성자, 관리자만 - @Transactional - public void delete(Long id, String email, Long orgId) { - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + Optional existing = noticeScrapRepository.findByMemberAndNoticePost( + memberProfile.getMember(), post); - NoticePost post = noticePostRepository.findById(id) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); + if (existing.isPresent()) { + noticeScrapRepository.delete(existing.get()); + post.decreaseScrapCount(); + return new NoticeScrapRespDto(false, null); + } - boolean isWriter = post.getWriter().getId().equals(memberProfile.getId()); - boolean isAdmin = (memberProfile.getMemberRole() == MemberRole.ADMIN); + NoticeScrap scrap = NoticeScrap.builder() + .member(memberProfile.getMember()) + .noticePost(post) + .savedAt(LocalDateTime.now()) + .build(); - if (!isAdmin && !isWriter) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_DELETE); - } + noticeScrapRepository.save(scrap); + post.increaseScrapCount(); - noticePostRepository.delete(post); - } + // 1. 알림 전송 + if (!post.getWriter().getId().equals(memberProfile.getId())) { + String message = memberProfile.getNickname() + "님이 글을 스크랩하였습니다."; - // [ 게시글 스크랩 ] - @Transactional - public NoticeScrapRespDto toggleScrap(Long postId, String email, Long orgId) { - MemberOrg memberProfile = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - NoticePost post = noticePostRepository.findById(postId) - .orElseThrow( () -> new PostNotFoundException(ErrorCode.POST_NOT_FOUND) ); - - // 같은 소속인지 체크 - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedCustomException(ErrorCode.ACCESS_DENIED_ORGANIZATION); - } - - Optional existing = noticeScrapRepository.findByMemberAndNoticePost(memberProfile.getMember(), post); - - if (existing.isPresent()) { - noticeScrapRepository.delete(existing.get()); - post.decreaseScrapCount(); - return new NoticeScrapRespDto(false, null); - } - - NoticeScrap scrap = NoticeScrap.builder() - .member(memberProfile.getMember()) - .noticePost(post) - .savedAt(LocalDateTime.now()) - .build(); - - noticeScrapRepository.save(scrap); - post.increaseScrapCount(); - - // 1. 알림 전송 - if(!post.getWriter().getId().equals(memberProfile.getId())){ - - String message = memberProfile.getNickname() + "님이 글을 스크랩하였습니다."; - - // 2. 알림 엔티티 생성 - Notification notification = Notification.builder() - .member(post.getWriter().getMember()) - .memberOrgId(post.getWriter().getId()) - .type(NotificationType.SCRAP) - .message(message) - .fromMemberOrgId(memberProfile.getId()) - .relatedUrl("/api/notice-posts/" + post.getId()) - .build(); - - //3. 알림 저장 및 전송을 위해 NotificationService 호출 - notificationService.send(notification); - } - return new NoticeScrapRespDto(true, scrap.getSavedAt()); - } + // 2. 알림 엔티티 생성 + Notification notification = Notification.builder() + .member(post.getWriter().getMember()) + .memberOrgId(post.getWriter().getId()) + .type(NotificationType.SCRAP) + .message(message) + .fromMemberOrgId(memberProfile.getId()) + .relatedUrl("/api/notice-posts/" + post.getId()) + .build(); - public void deleteExistingImages(NoticePost post) { - noticePostImageRepository.findByNoticePostId(post.getId()) - .forEach(oldImage -> { - s3UploadService.deleteImage(oldImage.getImageUrl()); - noticePostImageRepository.delete(oldImage); - }); + //3. 알림 저장 및 전송을 위해 NotificationService 호출 + notificationService.send(notification); } + return new NoticeScrapRespDto(true, scrap.getSavedAt()); + } + + public void deleteExistingImages(NoticePost post) { + noticePostImageRepository.findByNoticePostId(post.getId()) + .forEach(oldImage -> { + s3UploadService.deleteImage(oldImage.getImageUrl()); + noticePostImageRepository.delete(oldImage); + }); + } } diff --git a/src/main/java/org/example/tackit/domain/notification/service/NotificationService.java b/src/main/java/org/example/tackit/domain/notification/service/NotificationService.java index 09dac8e..931d47c 100644 --- a/src/main/java/org/example/tackit/domain/notification/service/NotificationService.java +++ b/src/main/java/org/example/tackit/domain/notification/service/NotificationService.java @@ -1,12 +1,17 @@ package org.example.tackit.domain.notification.service; import jakarta.persistence.EntityNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.example.tackit.common.dto.ProfileContext; import org.example.tackit.domain.admin.repository.AdminMemberRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.Org.MemberOrg; import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.notification.dto.NotificationEventDto; import org.example.tackit.domain.notification.dto.resp.NotificationRespDto; import org.example.tackit.domain.notification.repository.EmitterRepository; @@ -20,146 +25,140 @@ import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class NotificationService { - // 타임아웃 시간 - 150초 - private static final Long DEFAULT_TIMEOUT = 150L * 1000; - - private static final String EVENT_NAME_SSE = "sse"; - private static final String EVENT_NAME_NOTIFICATION = "notification"; - private static final String EVENT_NAME_HEARTBEAT = "heartbeat"; - - private final EmitterRepository emitterRepository; - private final ApplicationEventPublisher applicationEventPublisher; - private final NotificationRepository notificationRepository; - private final AdminMemberRepository adminMemberRepository; - private final MemberOrgRepository memberOrgRepository; - - // [ 클라이언트 -> 알림 구독 신청 ] - // 클라이언트가 서버에 처음 연결 요청 - public SseEmitter subscribe(Long userId) { - // 1. Emitter 생성 - SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); - emitterRepository.save(userId, emitter); - - // 2. 연결 끊어지거나 타임아웃되면 Emitter 제거하도록 - emitter.onCompletion(() -> emitterRepository.deleteById(userId)); - emitter.onTimeout(() -> emitterRepository.deleteById(userId)); - - // 3. 연결 생성 직후, 준비 완료되었다는 메세지 전송 - String connectMessage = "EventStream Created. [userId = " + userId + "]"; - sendEventToEmitter(emitter, userId, EVENT_NAME_SSE, connectMessage); - - return emitter; + // 타임아웃 시간 - 150초 + private static final Long DEFAULT_TIMEOUT = 150L * 1000; + + private static final String EVENT_NAME_SSE = "sse"; + private static final String EVENT_NAME_NOTIFICATION = "notification"; + private static final String EVENT_NAME_HEARTBEAT = "heartbeat"; + + private final EmitterRepository emitterRepository; + private final ApplicationEventPublisher applicationEventPublisher; + private final NotificationRepository notificationRepository; + private final AdminMemberRepository adminMemberRepository; + private final MemberOrgRepository memberOrgRepository; + + // [ 클라이언트 -> 알림 구독 신청 ] + // 클라이언트가 서버에 처음 연결 요청 + public SseEmitter subscribe(Long userId) { + // 1. Emitter 생성 + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); + emitterRepository.save(userId, emitter); + + // 2. 연결 끊어지거나 타임아웃되면 Emitter 제거하도록 + emitter.onCompletion(() -> emitterRepository.deleteById(userId)); + emitter.onTimeout(() -> emitterRepository.deleteById(userId)); + + // 3. 연결 생성 직후, 준비 완료되었다는 메세지 전송 + String connectMessage = "EventStream Created. [userId = " + userId + "]"; + sendEventToEmitter(emitter, userId, EVENT_NAME_SSE, connectMessage); + + return emitter; + } + + // [ 알림 이벤트 발생 ] + // 댓글이 달리는 등 알림 이벤트 발생 시, 시스템에 이벤트 발생 사실 전달 + @Transactional + public void send(Notification notification) { + // 1. 알림 DB에 저장 + notificationRepository.save(notification); + + // 2. 이벤트 발행 + applicationEventPublisher.publishEvent(new NotificationEventDto( + notification.getMember(), + notification.getType(), + notification.getMessage(), + notification.getRelatedUrl() + )); + } + + // [ 발행된 알림 이벤트를 수신받아 실제 클라이언트에게 알림 전달 ] + // 트랜잭션이 성공적으로 커밋된 후 실행 + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void deliverNotification(NotificationEventDto event) { + // 1. 이벤트에서 수신자 정보 가져오기 + Long userId = event.getReceiver().getId(); + String message = event.getMessage(); + + emitterRepository.findById(userId).ifPresent(emitter -> { + sendEventToEmitter(emitter, userId, EVENT_NAME_NOTIFICATION, message); + }); + } + + // [ 하트비트 전송 ] + // 주기적으로 하트비트 전송하여 연결 유지 및 좀비 커넥션 정리 + @Scheduled(fixedRate = 25000) // 25초마다 실행 + public void sendHeartbeat() { + Map emitters = emitterRepository.findAll(); + emitters.forEach((userId, emitter) -> { + sendEventToEmitter(emitter, userId, EVENT_NAME_HEARTBEAT, "HeartBeat OK"); + }); + } + + // [ 모든 알림 조회 ] + @Transactional(readOnly = true) + public List findAllNotifications(Long userId, ProfileContext profile) { + // 1. 읽지 않은 모든 알림 조회 + List notifications = notificationRepository.findAllByMemberIdAndMemberOrgIdOrderByCreatedAtDesc( + userId, profile.id()); + + if (notifications.isEmpty()) { + return Collections.emptyList(); } - // [ 알림 이벤트 발생 ] - // 댓글이 달리는 등 알림 이벤트 발생 시, 시스템에 이벤트 발생 사실 전달 - @Transactional - public void send(Notification notification) { - // 1. 알림 DB에 저장 - notificationRepository.save(notification); - - // 2. 이벤트 발행 - applicationEventPublisher.publishEvent(new NotificationEventDto( - notification.getMember(), - notification.getType(), - notification.getMessage(), - notification.getRelatedUrl() + // 2. 알림 보낸 프로필들의 ID 추출 + List fromOrgIds = notifications.stream() + .map(Notification::getFromMemberOrgId) + .distinct() + .toList(); + + // 3. 소속별 닉네임 한 번에 조회 + Map orgNicknameMap = memberOrgRepository.findAllById(fromOrgIds).stream() + .collect(Collectors.toMap( + MemberOrg::getId, + MemberOrg::getNickname )); - } - - // [ 발행된 알림 이벤트를 수신받아 실제 클라이언트에게 알림 전달 ] - // 트랜잭션이 성공적으로 커밋된 후 실행 - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void deliverNotification(NotificationEventDto event) { - // 1. 이벤트에서 수신자 정보 가져오기 - Long userId = event.getReceiver().getId(); - String message = event.getMessage(); - - emitterRepository.findById(userId).ifPresent(emitter -> { - sendEventToEmitter(emitter, userId, EVENT_NAME_NOTIFICATION, message); - }); - } - - // [ 하트비트 전송 ] - // 주기적으로 하트비트 전송하여 연결 유지 및 좀비 커넥션 정리 - @Scheduled(fixedRate = 25000) // 25초마다 실행 - public void sendHeartbeat() { - Map emitters = emitterRepository.findAll(); - emitters.forEach((userId, emitter) -> { - sendEventToEmitter(emitter, userId, EVENT_NAME_HEARTBEAT, "HeartBeat OK"); - }); - } - - // [ 모든 알림 조회 ] - @Transactional(readOnly = true) - public List findAllNotifications(Long userId, ProfileContext profile) { - // 1. 읽지 않은 모든 알림 조회 - List notifications = notificationRepository.findAllByMemberIdAndMemberOrgIdOrderByCreatedAtDesc(userId, profile.id()); - - if (notifications.isEmpty()) { - return Collections.emptyList(); - } - - // 2. 알림 보낸 프로필들의 ID 추출 - List fromOrgIds = notifications.stream() - .map(Notification::getFromMemberOrgId) - .distinct() - .toList(); - - - // 3. 소속별 닉네임 한 번에 조회 - Map orgNicknameMap = memberOrgRepository.findAllById(fromOrgIds).stream() - .collect(Collectors.toMap( - MemberOrg::getId, - MemberOrg::getNickname - )); - - // 4. 알림 데이터와 닉네임 맵 매칭 - - return notifications.stream() - .map(notification -> { - String fromNickname = orgNicknameMap.getOrDefault( - notification.getFromMemberOrgId(), "알 수 없는 사용자" - ); - return new NotificationRespDto(notification, fromNickname); - }) - .toList(); - } - - // [ 알림 읽음 ] - @Transactional - public void markAsRead(Long notificationId, String email) { - Notification notification = notificationRepository.findById(notificationId) - .orElseThrow(() -> new EntityNotFoundException("알림을 찾을 수 없습니다.")); - - // 본인의 알림이 맞는지 - if(!notification.getMember().getEmail().equals(email)) { - throw new AccessDeniedException("권한이 없습니다."); - } - notification.markAsRead(); + // 4. 알림 데이터와 닉네임 맵 매칭 + + return notifications.stream() + .map(notification -> { + String fromNickname = orgNicknameMap.getOrDefault( + notification.getFromMemberOrgId(), "알 수 없는 사용자" + ); + return new NotificationRespDto(notification, fromNickname); + }) + .toList(); + } + + // [ 알림 읽음 ] + @Transactional + public void markAsRead(Long notificationId, String email) { + Notification notification = notificationRepository.findById(notificationId) + .orElseThrow(() -> new EntityNotFoundException("알림을 찾을 수 없습니다.")); + + // 본인의 알림이 맞는지 + if (!notification.getMember().getEmail().equals(email)) { + throw new AccessDeniedException("권한이 없습니다."); } - // [ SseEmitter로 이벤트 전송하는 공통 메서드 ] - private void sendEventToEmitter(SseEmitter emitter, Long userId, String eventName, Object data) { - try { - emitter.send(SseEmitter.event() - .id(userId + "_" + System.currentTimeMillis()) - .name(eventName) - .data(data)); - } catch (IOException e) { - // 전송 중 예외 발생 시 Emitter 제거 - emitterRepository.deleteById(userId); - } + notification.markAsRead(); + } + + // [ SseEmitter로 이벤트 전송하는 공통 메서드 ] + private void sendEventToEmitter(SseEmitter emitter, Long userId, String eventName, Object data) { + try { + emitter.send(SseEmitter.event() + .id(userId + "_" + System.currentTimeMillis()) + .name(eventName) + .data(data)); + } catch (IOException e) { + // 전송 중 예외 발생 시 Emitter 제거 + emitterRepository.deleteById(userId); } + } } diff --git a/src/main/java/org/example/tackit/domain/organization/service/OrganizationService.java b/src/main/java/org/example/tackit/domain/organization/service/OrganizationService.java index 0c227bc..448214a 100644 --- a/src/main/java/org/example/tackit/domain/organization/service/OrganizationService.java +++ b/src/main/java/org/example/tackit/domain/organization/service/OrganizationService.java @@ -1,91 +1,98 @@ package org.example.tackit.domain.organization.service; +import java.time.LocalDate; +import java.util.NoSuchElementException; import lombok.AllArgsConstructor; +import org.example.tackit.domain.entity.Member; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Org.OrgStatus; +import org.example.tackit.domain.entity.Org.OrgType; +import org.example.tackit.domain.entity.Org.Organization; +import org.example.tackit.domain.entity.Org.School; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.member.repository.MemberRepository; import org.example.tackit.domain.organization.dto.req.OrgCreateReqDto; import org.example.tackit.domain.organization.dto.req.OrgJoinReqDto; import org.example.tackit.domain.organization.repository.OrganizationRepository; import org.example.tackit.domain.organization.repository.SchoolRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.*; import org.example.tackit.global.exception.ErrorCode; import org.example.tackit.global.exception.MemberNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; -import java.util.NoSuchElementException; - @Service @AllArgsConstructor public class OrganizationService { - private final MemberRepository memberRepository; - private final MemberOrgRepository memberOrgRepository; - private final SchoolRepository schoolRepository; - private final OrganizationRepository organizationRepository; - - // [ 모임 생성 ] - @Transactional - public void createOrg(OrgCreateReqDto dto, String email) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - Organization organization; - - // 타입별 체크 - if( dto.getOrgType() == OrgType.CLUB) { - School school = schoolRepository.findById(dto.getSchoolId()) - .orElseThrow(() -> new RuntimeException("해당 학교가 등록되어 있지 않습니다.")); - - // 같은 학교 내 동일한 이름의 동아리가 있는지 확인 - organizationRepository.findByNameAndSchoolAndType(dto.getOrgName(), school, OrgType.CLUB) - .ifPresent(o -> { throw new RuntimeException("해당 학교에 이미 동일한 이름의 동아리가 존재합니다."); }); - - // 엔티티 생성 및 저장 (학교 정보 포함) - organization = dto.toEntity(school); - } - else { - // 서비스 전체에서 동일한 이름의 소모임이 있는지 확인 - organizationRepository.findByNameAndType(dto.getOrgName(), OrgType.COMMUNITY) - .ifPresent(o -> { throw new RuntimeException("이미 동일한 이름의 소모임이 존재합니다."); }); - - // 엔티티 생성 및 저장 (학교 정보 null) - organization = dto.toEntity(null); - } - - organizationRepository.save(organization); + + private final MemberRepository memberRepository; + private final MemberOrgRepository memberOrgRepository; + private final SchoolRepository schoolRepository; + private final OrganizationRepository organizationRepository; + + // [ 모임 생성 ] + @Transactional + public void createOrg(OrgCreateReqDto dto, String email) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + Organization organization; + + // 타입별 체크 + if (dto.getOrgType() == OrgType.CLUB) { + School school = schoolRepository.findById(dto.getSchoolId()) + .orElseThrow(() -> new RuntimeException("해당 학교가 등록되어 있지 않습니다.")); + + // 같은 학교 내 동일한 이름의 동아리가 있는지 확인 + organizationRepository.findByNameAndSchoolAndType(dto.getOrgName(), school, OrgType.CLUB) + .ifPresent(o -> { + throw new RuntimeException("해당 학교에 이미 동일한 이름의 동아리가 존재합니다."); + }); + + // 엔티티 생성 및 저장 (학교 정보 포함) + organization = dto.toEntity(school); + } else { + // 서비스 전체에서 동일한 이름의 소모임이 있는지 확인 + organizationRepository.findByNameAndType(dto.getOrgName(), OrgType.COMMUNITY) + .ifPresent(o -> { + throw new RuntimeException("이미 동일한 이름의 소모임이 존재합니다."); + }); + + // 엔티티 생성 및 저장 (학교 정보 null) + organization = dto.toEntity(null); } - // [ 모임 참여 ] - @Transactional - public void joinOrg(Long orgId, String email, OrgJoinReqDto dto) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - // 조직 존재 유무 검토 - Organization organization = organizationRepository.findById(orgId) - .orElseThrow(() -> new NoSuchElementException("존재하지 않는 모임입니다. ID: " + orgId)); - - // 같은 모임 내 닉네임 중복 검토 - if ( memberOrgRepository.existsByOrganizationIdAndNickname(orgId, dto.getNickname())) { - throw new IllegalStateException("해당 모임에서 이미 사용 중인 닉네임입니다: " + dto.getNickname()); - } - - // 최초 가입자 여부 확인 - boolean isFirstMember = !memberOrgRepository.existsByOrganizationId(orgId); - - MemberOrg memberOrg = MemberOrg.builder() - .member(member) - .organization(organization) // 통합된 필드 사용 - .orgType(organization.getType()) - .nickname(dto.getNickname()) - .memberRole(dto.getMemberRole()) - .memberType(dto.getMemberType()) - .joinedYear(LocalDate.now().getYear()) - .orgStatus(isFirstMember ? OrgStatus.ACTIVE : OrgStatus.PENDING) // 삼항 연산자로 깔끔하게 - .build(); - - memberOrgRepository.save(memberOrg); + organizationRepository.save(organization); + } + + // [ 모임 참여 ] + @Transactional + public void joinOrg(Long orgId, String email, OrgJoinReqDto dto) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + // 조직 존재 유무 검토 + Organization organization = organizationRepository.findById(orgId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 모임입니다. ID: " + orgId)); + + // 같은 모임 내 닉네임 중복 검토 + if (memberOrgRepository.existsByOrganizationIdAndNickname(orgId, dto.getNickname())) { + throw new IllegalStateException("해당 모임에서 이미 사용 중인 닉네임입니다: " + dto.getNickname()); } + + // 최초 가입자 여부 확인 + boolean isFirstMember = !memberOrgRepository.existsByOrganizationId(orgId); + + MemberOrg memberOrg = MemberOrg.builder() + .member(member) + .organization(organization) // 통합된 필드 사용 + .orgType(organization.getType()) + .nickname(dto.getNickname()) + .memberRole(dto.getMemberRole()) + .memberType(dto.getMemberType()) + .joinedYear(LocalDate.now().getYear()) + .orgStatus(isFirstMember ? OrgStatus.ACTIVE : OrgStatus.PENDING) // 삼항 연산자로 깔끔하게 + .build(); + + memberOrgRepository.save(memberOrg); + } } diff --git a/src/main/java/org/example/tackit/domain/qnaBoard/QnA_comment/service/QnACommentService.java b/src/main/java/org/example/tackit/domain/qnaBoard/QnA_comment/service/QnACommentService.java index ab269a3..eb84eaf 100644 --- a/src/main/java/org/example/tackit/domain/qnaBoard/QnA_comment/service/QnACommentService.java +++ b/src/main/java/org/example/tackit/domain/qnaBoard/QnA_comment/service/QnACommentService.java @@ -1,138 +1,145 @@ package org.example.tackit.domain.qnaBoard.QnA_comment.service; import jakarta.persistence.EntityNotFoundException; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.QnAComment; +import org.example.tackit.domain.entity.QnAPost; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.notification.service.NotificationService; import org.example.tackit.domain.qnaBoard.QnA_comment.dto.request.QnACommentCreateDto; import org.example.tackit.domain.qnaBoard.QnA_comment.dto.request.QnACommentUpdateDto; import org.example.tackit.domain.qnaBoard.QnA_comment.dto.response.QnACommentResponseDto; import org.example.tackit.domain.qnaBoard.QnA_comment.repository.QnACommentRepository; import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAPostRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.notification.service.NotificationService; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; - @Service @RequiredArgsConstructor public class QnACommentService { - private final QnACommentRepository qnACommentRepository; - private final QnAPostRepository qnAPostRepository; - private final MemberOrgRepository memberOrgRepository; - private final NotificationService notificationService; - - // 댓글 생성 - @Transactional - public QnACommentResponseDto createComment(QnACommentCreateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - QnAPost post = qnAPostRepository.findById(dto.getQnaPostId()) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - - QnAComment comment = QnAComment.builder() - .writer(member) - .accountStatus(AccountStatus.ACTIVE) - .qnAPost(post) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .build(); - - // 댓글 DB 저장 - QnAComment savedComment = qnACommentRepository.save(comment); - - // 알림 전송 - if (!post.getWriter().getId().equals(member.getId())) { - MemberOrg postWriter = post.getWriter(); - - notificationService.send(Notification.builder() - .member(postWriter.getMember()) // 수신자 계정 - .memberOrgId(postWriter.getId()) // 수신자 프로필 ID - .type(NotificationType.COMMENT) - .message(member.getNickname() + "님이 QnA 글에 댓글을 남겼습니다.") - .relatedUrl("/api/qna-posts/" + post.getId()) - .fromMemberOrgId(member.getId()) // 보낸 사람 프로필 ID - .build()); - } - return new QnACommentResponseDto(savedComment); + private final QnACommentRepository qnACommentRepository; + private final QnAPostRepository qnAPostRepository; + private final MemberOrgRepository memberOrgRepository; + private final NotificationService notificationService; + + // 댓글 생성 + @Transactional + public QnACommentResponseDto createComment(QnACommentCreateDto dto, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + QnAPost post = qnAPostRepository.findById(dto.getQnaPostId()) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + + QnAComment comment = QnAComment.builder() + .writer(member) + .accountStatus(AccountStatus.ACTIVE) + .qnAPost(post) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .build(); + + // 댓글 DB 저장 + QnAComment savedComment = qnACommentRepository.save(comment); + + // 알림 전송 + if (!post.getWriter().getId().equals(member.getId())) { + MemberOrg postWriter = post.getWriter(); + + notificationService.send(Notification.builder() + .member(postWriter.getMember()) // 수신자 계정 + .memberOrgId(postWriter.getId()) // 수신자 프로필 ID + .type(NotificationType.COMMENT) + .message(member.getNickname() + "님이 QnA 글에 댓글을 남겼습니다.") + .relatedUrl("/api/qna-posts/" + post.getId()) + .fromMemberOrgId(member.getId()) // 보낸 사람 프로필 ID + .build()); } + return new QnACommentResponseDto(savedComment); - // 전체 댓글 조회 - @Transactional (readOnly = true) - public List getCommentByPost(Long postId, Long orgId){ - QnAPost post = qnAPostRepository.findById(postId) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + } - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); - } + // 전체 댓글 조회 + @Transactional(readOnly = true) + public List getCommentByPost(Long postId, Long orgId) { + QnAPost post = qnAPostRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - return qnACommentRepository.findByQnAPost(post) - .stream() - .map(QnACommentResponseDto::new) - .toList(); + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); } - // 댓글 수정 (작성자만 가능) - @Transactional - public QnACommentResponseDto updateComment(Long commentId, QnACommentUpdateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - QnAComment comment = qnACommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + return qnACommentRepository.findByQnAPost(post) + .stream() + .map(QnACommentResponseDto::new) + .toList(); + } - boolean isWriter = comment.getWriter().getId().equals(member.getId()); + // 댓글 수정 (작성자만 가능) + @Transactional + public QnACommentResponseDto updateComment(Long commentId, QnACommentUpdateDto dto, String email, + Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if (!isWriter ) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + QnAComment comment = qnACommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - comment.updateContent(dto.getContent()); + boolean isWriter = comment.getWriter().getId().equals(member.getId()); - return new QnACommentResponseDto(comment); + if (!isWriter) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } - // 댓글 삭제 (작성자, 관리자만 가능) - @Transactional - public void deleteComment(Long commentId, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + comment.updateContent(dto.getContent()); - QnAComment comment = qnACommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + return new QnACommentResponseDto(comment); + } - boolean isWriter = comment.getWriter().getId().equals(member.getId()); - boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) - && (member.getMemberType() == MemberType.ADMIN); + // 댓글 삭제 (작성자, 관리자만 가능) + @Transactional + public void deleteComment(Long commentId, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if (!isWriter && !isAdmin) { - throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); - } + QnAComment comment = qnACommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - qnACommentRepository.delete(comment); // hard delete + boolean isWriter = comment.getWriter().getId().equals(member.getId()); + boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) + && (member.getMemberType() == MemberType.ADMIN); + + if (!isWriter && !isAdmin) { + throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); } - // 댓글 신고하기 - @Transactional - public void increaseCommentReportCount(Long id, Long orgId) { - QnAComment comment = qnACommentRepository.findById(id) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + qnACommentRepository.delete(comment); // hard delete + } + + // 댓글 신고하기 + @Transactional + public void increaseCommentReportCount(Long id, Long orgId) { + QnAComment comment = qnACommentRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - if (!comment.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); - } - comment.increaseReportCount(); + if (!comment.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); + } + comment.increaseReportCount(); - if (comment.getReportCount() >= 3) { - qnACommentRepository.delete(comment); // hard delete - } + if (comment.getReportCount() >= 3) { + qnACommentRepository.delete(comment); // hard delete } + } } diff --git a/src/main/java/org/example/tackit/domain/qnaBoard/QnA_post/service/QnAPostService.java b/src/main/java/org/example/tackit/domain/qnaBoard/QnA_post/service/QnAPostService.java index 711752e..032807e 100644 --- a/src/main/java/org/example/tackit/domain/qnaBoard/QnA_post/service/QnAPostService.java +++ b/src/main/java/org/example/tackit/domain/qnaBoard/QnA_post/service/QnAPostService.java @@ -1,7 +1,21 @@ package org.example.tackit.domain.qnaBoard.QnA_post.service; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.example.tackit.common.dto.PageResponseDTO; import org.example.tackit.config.S3.S3UploadService; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Post; +import org.example.tackit.domain.entity.QnAPost; +import org.example.tackit.domain.entity.QnAPostImage; +import org.example.tackit.domain.entity.QnAReport; +import org.example.tackit.domain.member.repository.MemberOrgRepository; import org.example.tackit.domain.qnaBoard.QnA_post.dto.request.QnAPostReqDto; import org.example.tackit.domain.qnaBoard.QnA_post.dto.request.UpdateQnARequestDto; import org.example.tackit.domain.qnaBoard.QnA_post.dto.response.QnAPopularPostRespDto; @@ -9,10 +23,6 @@ import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAPostReportRepository; import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAPostRepository; import org.example.tackit.domain.qnaBoard.QnA_post.repository.QnAScrapRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.common.dto.PageResponseDTO; -import org.example.tackit.domain.entity.Org.MemberOrg; import org.example.tackit.global.exception.ErrorCode; import org.example.tackit.global.exception.MemberNotFoundException; import org.springframework.data.domain.Page; @@ -21,185 +31,183 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - @Service @RequiredArgsConstructor public class QnAPostService { - private final QnAPostRepository qnAPostRepository; - private final MemberOrgRepository memberOrgRepository; - private final QnAPostTagService tagService; - private final QnAPostReportRepository qnAPostReportRepository; - private final S3UploadService s3UploadService; - private final QnAScrapRepository qnAScrapRepository; - - // 게시글 작성 (NEWBIE만 가능) - @Transactional - public QnAPostRespDto createPost(QnAPostReqDto dto, String email, Long orgId) throws IOException { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - if (member.getMemberType() != MemberType.NEWBIE) { - throw new AccessDeniedException("NEWBIE만 질문을 작성할 수 있습니다."); - } - - QnAPost post = QnAPost.builder() - .writer(member) - .title(dto.getTitle()) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .type(Post.QnA) - .accountStatus(AccountStatus.ACTIVE) - .reportCount(0) - .isAnonymous(dto.isAnonymous()) - .build(); - - // 이미지가 있으면 추가 - if (dto.getImageUrl() != null && !dto.getImageUrl().isEmpty()) { - String imageUrl = s3UploadService.saveFile(dto.getImageUrl()); - QnAPostImage image = new QnAPostImage(); - image.setImageUrl(imageUrl); - image.setPost(post); - post.addImage(image); - } + private final QnAPostRepository qnAPostRepository; + private final MemberOrgRepository memberOrgRepository; + private final QnAPostTagService tagService; + private final QnAPostReportRepository qnAPostReportRepository; + private final S3UploadService s3UploadService; + private final QnAScrapRepository qnAScrapRepository; + + // 게시글 작성 (NEWBIE만 가능) + @Transactional + public QnAPostRespDto createPost(QnAPostReqDto dto, String email, Long orgId) throws IOException { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + if (member.getMemberType() != MemberType.NEWBIE) { + throw new AccessDeniedException("NEWBIE만 질문을 작성할 수 있습니다."); + } - qnAPostRepository.save(post); + QnAPost post = QnAPost.builder() + .writer(member) + .title(dto.getTitle()) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .type(Post.QnA) + .accountStatus(AccountStatus.ACTIVE) + .reportCount(0) + .isAnonymous(dto.isAnonymous()) + .build(); + + // 이미지가 있으면 추가 + if (dto.getImageUrl() != null && !dto.getImageUrl().isEmpty()) { + String imageUrl = s3UploadService.saveFile(dto.getImageUrl()); + QnAPostImage image = new QnAPostImage(); + image.setImageUrl(imageUrl); + image.setPost(post); + post.addImage(image); + } - List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); + qnAPostRepository.save(post); - return QnAPostRespDto.fromEntity(post, tagNames, false); - } + List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); - // 게시글 수정 (작성자만 가능) - @Transactional - public QnAPostRespDto update(Long id, UpdateQnARequestDto request, String email, Long orgId) throws IOException { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + return QnAPostRespDto.fromEntity(post, tagNames, false); + } - QnAPost post = qnAPostRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); + // 게시글 수정 (작성자만 가능) + @Transactional + public QnAPostRespDto update(Long id, UpdateQnARequestDto request, String email, Long orgId) + throws IOException { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - boolean isWriter = post.getWriter().getId().equals(member.getId()); + QnAPost post = qnAPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); - if (!isWriter) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + boolean isWriter = post.getWriter().getId().equals(member.getId()); - post.update(request.getTitle(), request.getContent()); - - tagService.deleteTagsByPost(post); // 기존 태그 삭제 - List tagNames = tagService.assignTagsToPost(post, request.getTagIds()); // 새 태그 등록 - - // 이미지 수정 로직 - if (Boolean.TRUE.equals(request.getRemoveImage())) { - // 이미지 삭제 - post.getImages().forEach(img -> { - if (img.getImageUrl() != null) { - s3UploadService.deleteImage(img.getImageUrl()); - } - }); - post.clearImages(); - - } else if (request.getImage() != null && !request.getImage().isEmpty()) { - // 이미지 교체/추가 - post.clearImages(); - String imageUrl = s3UploadService.saveFile(request.getImage()); - QnAPostImage image = new QnAPostImage(); - image.setImageUrl(imageUrl); - image.setPost(post); - post.addImage(image); - } // 이미지 유지 - - return QnAPostRespDto.fromEntity(post, tagNames, false); + if (!isWriter) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } - // 게시글 삭제 (작성자, 관리자만 가능) - @Transactional - public void delete(long id, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + post.update(request.getTitle(), request.getContent()); - QnAPost post = qnAPostRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); + tagService.deleteTagsByPost(post); // 기존 태그 삭제 + List tagNames = tagService.assignTagsToPost(post, request.getTagIds()); // 새 태그 등록 - boolean isWriter = post.getWriter().getId().equals(member.getId()); - boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) - && (member.getMemberType() == MemberType.ADMIN); - - if (!isWriter && !isAdmin) { - throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); + // 이미지 수정 로직 + if (Boolean.TRUE.equals(request.getRemoveImage())) { + // 이미지 삭제 + post.getImages().forEach(img -> { + if (img.getImageUrl() != null) { + s3UploadService.deleteImage(img.getImageUrl()); } - // tagService.deleteTagsByPost(post); - post.markAsDeleted(); //Deleted로 -> soft delete + }); + post.clearImages(); + + } else if (request.getImage() != null && !request.getImage().isEmpty()) { + // 이미지 교체/추가 + post.clearImages(); + String imageUrl = s3UploadService.saveFile(request.getImage()); + QnAPostImage image = new QnAPostImage(); + image.setImageUrl(imageUrl); + image.setPost(post); + post.addImage(image); + } // 이미지 유지 + + return QnAPostRespDto.fromEntity(post, tagNames, false); + } + + // 게시글 삭제 (작성자, 관리자만 가능) + @Transactional + public void delete(long id, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + QnAPost post = qnAPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); + + boolean isWriter = post.getWriter().getId().equals(member.getId()); + boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) + && (member.getMemberType() == MemberType.ADMIN); + + if (!isWriter && !isAdmin) { + throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); } + // tagService.deleteTagsByPost(post); + post.markAsDeleted(); //Deleted로 -> soft delete + } - // 게시글 전체 조회 - public PageResponseDTO findAll(Long orgId, Pageable pageable) { - Page page = qnAPostRepository.findByWriterIdAndAccountStatus(orgId, AccountStatus.ACTIVE, pageable); - List posts = page.getContent(); + // 게시글 전체 조회 + public PageResponseDTO findAll(Long orgId, Pageable pageable) { + Page page = qnAPostRepository.findByWriterIdAndAccountStatus(orgId, + AccountStatus.ACTIVE, pageable); + List posts = page.getContent(); - Map> tagMap = tagService.getTagNamesByPosts(posts); + Map> tagMap = tagService.getTagNamesByPosts(posts); - return PageResponseDTO.from(page, post -> { - List tagNames = tagMap.getOrDefault(post.getId(), List.of()); - // 전체 조회 시에는 스크랩 여부 false 값으로 고정 - return QnAPostRespDto.fromEntity(post, tagNames, false); - }); - } + return PageResponseDTO.from(page, post -> { + List tagNames = tagMap.getOrDefault(post.getId(), List.of()); + // 전체 조회 시에는 스크랩 여부 false 값으로 고정 + return QnAPostRespDto.fromEntity(post, tagNames, false); + }); + } - // 게시글 상세 조회 - public QnAPostRespDto getPostById(Long id, Long orgId, Long memberId) { - QnAPost post = qnAPostRepository.findById(id) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + // 게시글 상세 조회 + public QnAPostRespDto getPostById(Long id, Long orgId, Long memberId) { + QnAPost post = qnAPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); - } + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); + } - post.increaseViewCount(); - List tagNames = tagService.getTagNamesByPost(post); + post.increaseViewCount(); + List tagNames = tagService.getTagNamesByPost(post); - // 스크랩 여부 조회 - boolean isScrap = qnAScrapRepository.existsByQnaPostIdAndMemberId(id, memberId); + // 스크랩 여부 조회 + boolean isScrap = qnAScrapRepository.existsByQnaPostIdAndMemberId(id, memberId); - return QnAPostRespDto.fromEntity(post, tagNames, isScrap); - } + return QnAPostRespDto.fromEntity(post, tagNames, isScrap); + } - // 게시글 신고하기 - @Transactional - public String reportQnAPost(Long postId, Long orgId) { - QnAPost post = qnAPostRepository.findById(postId) - .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + // 게시글 신고하기 + @Transactional + public String reportQnAPost(Long postId, Long orgId) { + QnAPost post = qnAPostRepository.findById(postId) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - MemberOrg member = memberOrgRepository.findById(orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + MemberOrg member = memberOrgRepository.findById(orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - if (qnAPostReportRepository.existsByMemberAndQnaPost(member, post)) { - return "이미 신고한 게시글입니다."; - } + if (qnAPostReportRepository.existsByMemberAndQnaPost(member, post)) { + return "이미 신고한 게시글입니다."; + } - qnAPostReportRepository.save(QnAReport.builder() - .member(member) - .qnaPost(post) - .build()); + qnAPostReportRepository.save(QnAReport.builder() + .member(member) + .qnaPost(post) + .build()); - post.increaseReportCount(); + post.increaseReportCount(); - return "게시글을 신고하였습니다."; - } + return "게시글을 신고하였습니다."; + } - // 인기 3개 - @Transactional(readOnly = true) - public List getPopularPosts(Long orgId) { - return qnAPostRepository.findTop3ByWriterIdAndAccountStatusOrderByViewCountDescScrapCountDesc(orgId, AccountStatus.ACTIVE) - .stream() - .map(QnAPopularPostRespDto::from) - .toList(); + // 인기 3개 + @Transactional(readOnly = true) + public List getPopularPosts(Long orgId) { + return qnAPostRepository.findTop3ByWriterIdAndAccountStatusOrderByViewCountDescScrapCountDesc( + orgId, AccountStatus.ACTIVE) + .stream() + .map(QnAPopularPostRespDto::from) + .toList(); /* .stream() .filter(post -> post.getWriter().getOrganization().equals(organization)) @@ -218,7 +226,7 @@ public List getPopularPosts(Long orgId) { .toList(); */ - } + } } diff --git a/src/main/java/org/example/tackit/domain/report/service/ReportService.java b/src/main/java/org/example/tackit/domain/report/service/ReportService.java index 9d2dce1..e2c2916 100644 --- a/src/main/java/org/example/tackit/domain/report/service/ReportService.java +++ b/src/main/java/org/example/tackit/domain/report/service/ReportService.java @@ -1,11 +1,7 @@ package org.example.tackit.domain.report.service; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.example.tackit.domain.auth.login.repository.MemberRepository; -import org.example.tackit.domain.entity.Member; -import org.example.tackit.domain.entity.Report; -import org.example.tackit.domain.report.dto.ReportRequestDto; +import org.example.tackit.domain.member.repository.MemberRepository; import org.example.tackit.domain.report.repository.ReportRepository; import org.springframework.stereotype.Service; @@ -13,8 +9,9 @@ @Service @RequiredArgsConstructor public class ReportService { - private final ReportRepository reportRepository; - private final MemberRepository memberRepository; + + private final ReportRepository reportRepository; + private final MemberRepository memberRepository; /* @Transactional diff --git a/src/main/java/org/example/tackit/domain/tipBoard/Tip_comment/service/TipCommentService.java b/src/main/java/org/example/tackit/domain/tipBoard/Tip_comment/service/TipCommentService.java index cc69bc0..247a9ad 100644 --- a/src/main/java/org/example/tackit/domain/tipBoard/Tip_comment/service/TipCommentService.java +++ b/src/main/java/org/example/tackit/domain/tipBoard/Tip_comment/service/TipCommentService.java @@ -1,143 +1,150 @@ package org.example.tackit.domain.tipBoard.Tip_comment.service; import jakarta.persistence.EntityNotFoundException; +import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.MemberRole; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.TipComment; +import org.example.tackit.domain.entity.TipPost; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.notification.service.NotificationService; import org.example.tackit.domain.tipBoard.Tip_comment.dto.req.TipCommentCreateDto; import org.example.tackit.domain.tipBoard.Tip_comment.dto.req.TipCommentUpdateDto; import org.example.tackit.domain.tipBoard.Tip_comment.dto.resp.TipCommentResponseDto; import org.example.tackit.domain.tipBoard.Tip_comment.repository.TipCommentRepository; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipPostRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.notification.service.NotificationService; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.List; - @Service @RequiredArgsConstructor public class TipCommentService { - private final TipCommentRepository tipCommentRepository; - private final TipPostRepository tipPostRepository; - private final NotificationService notificationService; - private final MemberOrgRepository memberOrgRepository; - - // 댓글 생성 (SENIOR + NEWBIE 둘 다) - @Transactional - public TipCommentResponseDto createComment(TipCommentCreateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - TipPost post = tipPostRepository.findById(dto.getTipPostId()) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - - TipComment comment = TipComment.builder() - .writer(member) - .accountStatus(AccountStatus.ACTIVE) - .tipPost(post) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .build(); - - // 댓글 DB 저장 - TipComment savedComment = tipCommentRepository.save(comment); - - // 알림 전송 - if(!post.getWriter().getId().equals(member.getId())) { - MemberOrg postWriter = post.getWriter(); - - String message = member.getNickname() + "님이 글에 댓글을 남겼습니다."; - String url = "/api/tip-post/" + post.getId(); - - // 알림 엔티티 생성 - Notification notification = Notification.builder() - .member(postWriter.getMember()) - .type(NotificationType.COMMENT) - .message(message) - .relatedUrl(url) - .fromMemberOrgId(member.getId()) - .build(); - - // 알림 저장 및 전송 - notificationService.send(notification); - } - return new TipCommentResponseDto(savedComment); - } + private final TipCommentRepository tipCommentRepository; + private final TipPostRepository tipPostRepository; + private final NotificationService notificationService; + private final MemberOrgRepository memberOrgRepository; + + // 댓글 생성 (SENIOR + NEWBIE 둘 다) + @Transactional + public TipCommentResponseDto createComment(TipCommentCreateDto dto, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + TipPost post = tipPostRepository.findById(dto.getTipPostId()) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); + + TipComment comment = TipComment.builder() + .writer(member) + .accountStatus(AccountStatus.ACTIVE) + .tipPost(post) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .build(); + + // 댓글 DB 저장 + TipComment savedComment = tipCommentRepository.save(comment); + + // 알림 전송 + if (!post.getWriter().getId().equals(member.getId())) { + MemberOrg postWriter = post.getWriter(); + + String message = member.getNickname() + "님이 글에 댓글을 남겼습니다."; + String url = "/api/tip-post/" + post.getId(); + + // 알림 엔티티 생성 + Notification notification = Notification.builder() + .member(postWriter.getMember()) + .type(NotificationType.COMMENT) + .message(message) + .relatedUrl(url) + .fromMemberOrgId(member.getId()) + .build(); + + // 알림 저장 및 전송 + notificationService.send(notification); + } + return new TipCommentResponseDto(savedComment); + } - // 전체 댓글 조회 - @Transactional (readOnly = true) - public List getCommentByPost(Long postId, Long orgId) { - TipPost post = tipPostRepository.findById(postId) - .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - if (!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); - } + // 전체 댓글 조회 + @Transactional(readOnly = true) + public List getCommentByPost(Long postId, Long orgId) { + TipPost post = tipPostRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다.")); - return tipCommentRepository.findByTipPost(post) - .stream() - .map(TipCommentResponseDto::new) - .toList(); + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글만 조회할 수 있습니다."); } - // 댓글 수정 (작성자만 가능) - @Transactional - public TipCommentResponseDto updateComment(Long commentId, TipCommentUpdateDto dto, String email, Long orgId){ - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + return tipCommentRepository.findByTipPost(post) + .stream() + .map(TipCommentResponseDto::new) + .toList(); + } - TipComment comment = tipCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + // 댓글 수정 (작성자만 가능) + @Transactional + public TipCommentResponseDto updateComment(Long commentId, TipCommentUpdateDto dto, String email, + Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - if( !comment.getWriter().getId().equals(member.getId())) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + TipComment comment = tipCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); - comment.updateContent(dto.getContent()); - - return new TipCommentResponseDto(comment); + if (!comment.getWriter().getId().equals(member.getId())) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } - // 댓글 삭제 (작성자, 관리자만 가능) - @Transactional - public void deleteComment(Long commentId, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + comment.updateContent(dto.getContent()); + + return new TipCommentResponseDto(comment); + } - TipComment comment = tipCommentRepository.findById(commentId) - .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + // 댓글 삭제 (작성자, 관리자만 가능) + @Transactional + public void deleteComment(Long commentId, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + TipComment comment = tipCommentRepository.findById(commentId) + .orElseThrow(() -> new EntityNotFoundException("댓글이 존재하지 않습니다.")); + + boolean isWriter = comment.getWriter().getId().equals(member.getId()); + boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) + && (member.getMemberType() == MemberType.ADMIN); + + if (!isWriter && !isAdmin) { + throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); + } - boolean isWriter = comment.getWriter().getId().equals(member.getId()); - boolean isAdmin = (member.getMemberRole() == MemberRole.ADMIN) - && (member.getMemberType() == MemberType.ADMIN); + tipCommentRepository.delete(comment); // hard delete + } - if (!isWriter && !isAdmin) { - throw new AccessDeniedException("작성자 또는 관리자만 삭제할 수 있습니다."); - } + // 댓글 신고하기 + @Transactional + public void increaseCommentReportCount(Long commentId, Long orgId) { + TipComment comment = tipCommentRepository.findById(commentId) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - tipCommentRepository.delete(comment); // hard delete + // 소속 검증 + if (!comment.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); } + comment.increaseReportCount(); - // 댓글 신고하기 - @Transactional - public void increaseCommentReportCount(Long commentId, Long orgId) { - TipComment comment = tipCommentRepository.findById(commentId) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - - // 소속 검증 - if (!comment.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 댓글만 신고할 수 있습니다."); - } - comment.increaseReportCount(); - - if (comment.getReportCount() >= 3) { - tipCommentRepository.delete(comment); // hard delete - } + if (comment.getReportCount() >= 3) { + tipCommentRepository.delete(comment); // hard delete } + } } \ No newline at end of file diff --git a/src/main/java/org/example/tackit/domain/tipBoard/Tip_post/service/TipPostService.java b/src/main/java/org/example/tackit/domain/tipBoard/Tip_post/service/TipPostService.java index 63087c1..24cb284 100644 --- a/src/main/java/org/example/tackit/domain/tipBoard/Tip_post/service/TipPostService.java +++ b/src/main/java/org/example/tackit/domain/tipBoard/Tip_post/service/TipPostService.java @@ -2,22 +2,34 @@ import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.example.tackit.common.dto.PageResponseDTO; import org.example.tackit.config.S3.S3UploadService; +import org.example.tackit.domain.entity.AccountStatus; +import org.example.tackit.domain.entity.MemberType; +import org.example.tackit.domain.entity.Notification; +import org.example.tackit.domain.entity.NotificationType; +import org.example.tackit.domain.entity.Org.MemberOrg; +import org.example.tackit.domain.entity.Post; +import org.example.tackit.domain.entity.TipPost; +import org.example.tackit.domain.entity.TipPostImage; +import org.example.tackit.domain.entity.TipReport; +import org.example.tackit.domain.entity.TipScrap; +import org.example.tackit.domain.member.repository.MemberOrgRepository; +import org.example.tackit.domain.notification.service.NotificationService; +import org.example.tackit.domain.tipBoard.Tip_post.dto.request.TipPostReqDto; +import org.example.tackit.domain.tipBoard.Tip_post.dto.request.TipPostUpdateDto; import org.example.tackit.domain.tipBoard.Tip_post.dto.response.TipPopularPostRespDto; import org.example.tackit.domain.tipBoard.Tip_post.dto.response.TipPostRespDto; import org.example.tackit.domain.tipBoard.Tip_post.dto.response.TipScrapRespDto; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipPostReportRepository; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipPostRepository; -import org.example.tackit.domain.tipBoard.Tip_tag.repository.TipPostTagMapRepository; -import org.example.tackit.domain.auth.login.repository.MemberOrgRepository; -import org.example.tackit.domain.entity.*; -import org.example.tackit.domain.tipBoard.Tip_post.dto.request.TipPostReqDto; -import org.example.tackit.domain.tipBoard.Tip_post.dto.request.TipPostUpdateDto; import org.example.tackit.domain.tipBoard.Tip_post.repository.TipScrapRepository; -import org.example.tackit.domain.entity.Org.MemberOrg; -import org.example.tackit.domain.notification.service.NotificationService; -import org.example.tackit.common.dto.PageResponseDTO; +import org.example.tackit.domain.tipBoard.Tip_tag.repository.TipPostTagMapRepository; import org.example.tackit.global.exception.ErrorCode; import org.example.tackit.global.exception.MemberNotFoundException; import org.springframework.data.domain.Page; @@ -26,57 +38,54 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - @Service @RequiredArgsConstructor public class TipPostService { - private final TipPostRepository tipPostRepository; - private final MemberOrgRepository memberOrgRepository; - private final TipScrapRepository tipScrapRepository; - private final TipPostReportRepository tipPostReportRepository; - private final TipPostTagMapRepository tipPostTagMapRepository; - private final TipTagService tagService; - private final S3UploadService s3UploadService; - private final NotificationService notificationService; - - public PageResponseDTO getActivePostsByOrganization(Long orgId, Pageable pageable) { - Page page = tipPostRepository.findByWriterId(orgId, pageable); - - return PageResponseDTO.from(page, post -> { - List tags = tipPostTagMapRepository.findByTipPost(post).stream() - .map(mapping -> mapping.getTag().getTagName()) - .toList(); - - boolean anonymous = post.isAnonymous(); - - return TipPostRespDto.builder() - .id(post.getId()) - .writer(anonymous ? "익명" : post.getWriter().getNickname()) - .profileImageUrl(anonymous ? null : post.getWriter().getProfileImageUrl()) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tags) - .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) - .isAnonymous(anonymous) - .build(); - }); - } - - // [ 게시글 상세 조회 ] - @Transactional - public TipPostRespDto getPostById(Long id, Long orgId, Long memberId) { - TipPost tipPost = tipPostRepository.findById(id) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - // 현재 접속한 소속(orgId)의 글이 맞는지 체크 - if (!tipPost.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직의 게시글이 아닙니다."); - } + private final TipPostRepository tipPostRepository; + private final MemberOrgRepository memberOrgRepository; + private final TipScrapRepository tipScrapRepository; + private final TipPostReportRepository tipPostReportRepository; + private final TipPostTagMapRepository tipPostTagMapRepository; + private final TipTagService tagService; + private final S3UploadService s3UploadService; + private final NotificationService notificationService; + + public PageResponseDTO getActivePostsByOrganization(Long orgId, + Pageable pageable) { + Page page = tipPostRepository.findByWriterId(orgId, pageable); + + return PageResponseDTO.from(page, post -> { + List tags = tipPostTagMapRepository.findByTipPost(post).stream() + .map(mapping -> mapping.getTag().getTagName()) + .toList(); + + boolean anonymous = post.isAnonymous(); + + return TipPostRespDto.builder() + .id(post.getId()) + .writer(anonymous ? "익명" : post.getWriter().getNickname()) + .profileImageUrl(anonymous ? null : post.getWriter().getProfileImageUrl()) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tags) + .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) + .isAnonymous(anonymous) + .build(); + }); + } + + // [ 게시글 상세 조회 ] + @Transactional + public TipPostRespDto getPostById(Long id, Long orgId, Long memberId) { + TipPost tipPost = tipPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + + // 현재 접속한 소속(orgId)의 글이 맞는지 체크 + if (!tipPost.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직의 게시글이 아닙니다."); + } /* if (!tipPost.getAccountStatus().equals(AccountStatus.ACTIVE)) { @@ -85,245 +94,248 @@ public TipPostRespDto getPostById(Long id, Long orgId, Long memberId) { */ - tipPost.increaseViewCount(); - - List tagNames = tagService.getTagNamesByPost(tipPost); - - // 스크랩 여부 조회 - boolean isScrap = tipScrapRepository.existsByTipPostIdAndMemberOrg_Id(id, memberId); - - // 익명 여부 조회 - boolean anonymous = tipPost.isAnonymous(); - - return TipPostRespDto.builder() - .id(tipPost.getId()) - .writer(anonymous ? "익명" : tipPost.getWriter().getNickname()) - .profileImageUrl(anonymous ? null : tipPost.getWriter().getProfileImageUrl()) - .title(tipPost.getTitle()) - .content(tipPost.getContent()) - .tags(tagNames) - .imageUrl(tipPost.getImages().isEmpty() ? null : tipPost.getImages().get(0).getImageUrl()) - .createdAt(tipPost.getCreatedAt()) - .isScrap(isScrap) - .isAnonymous(anonymous) - .build(); + tipPost.increaseViewCount(); + + List tagNames = tagService.getTagNamesByPost(tipPost); + + // 스크랩 여부 조회 + boolean isScrap = tipScrapRepository.existsByTipPostIdAndMemberOrg_Id(id, memberId); + + // 익명 여부 조회 + boolean anonymous = tipPost.isAnonymous(); + + return TipPostRespDto.builder() + .id(tipPost.getId()) + .writer(anonymous ? "익명" : tipPost.getWriter().getNickname()) + .profileImageUrl(anonymous ? null : tipPost.getWriter().getProfileImageUrl()) + .title(tipPost.getTitle()) + .content(tipPost.getContent()) + .tags(tagNames) + .imageUrl(tipPost.getImages().isEmpty() ? null : tipPost.getImages().get(0).getImageUrl()) + .createdAt(tipPost.getCreatedAt()) + .isScrap(isScrap) + .isAnonymous(anonymous) + .build(); + } + + // [ 게시글 작성 ] : 선배만 가능 + @Transactional + public TipPostRespDto createPost(TipPostReqDto dto, String email, Long orgId, MultipartFile image) + throws IOException { + // 1. 유저 조회 + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + if (member.getMemberType() != MemberType.SENIOR) { + throw new AccessDeniedException("SENIOR만 게시글을 작성할 수 있습니다."); } - // [ 게시글 작성 ] : 선배만 가능 - @Transactional - public TipPostRespDto createPost(TipPostReqDto dto, String email, Long orgId, MultipartFile image) throws IOException { - // 1. 유저 조회 - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - if (member.getMemberType() != MemberType.SENIOR) { - throw new AccessDeniedException("SENIOR만 게시글을 작성할 수 있습니다."); - } - - // 2. 게시글 생성 - TipPost post = TipPost.builder() - .writer(member) - .title(dto.getTitle()) - .content(dto.getContent()) - .createdAt(LocalDateTime.now()) - .type(Post.Tip) - .accountStatus(AccountStatus.ACTIVE) - .reportCount(0) - .isAnonymous(dto.isAnonymous()) - .build(); - - // 3. 이미지 업로드 & 연관관계 매핑 (단일 파일만) - if (image != null && !image.isEmpty()) { - String imageUrl = s3UploadService.saveFile(image); - TipPostImage imageEntity = TipPostImage.builder() - .imageUrl(imageUrl) - .build(); - post.addImage(imageEntity); // 기존 이미지 clear 후 하나만 저장 - } - - tipPostRepository.save(post); - - List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); - - boolean anonymous = post.isAnonymous(); - - // 응답 DTO 구성 (imageUrl 하나만) - return TipPostRespDto.builder() - .id(post.getId()) - .writer(anonymous ? "익명" : member.getNickname()) - .profileImageUrl(anonymous ? null : member.getProfileImageUrl()) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tagNames) - .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) - .isAnonymous(anonymous) - .isScrap(false) - .build(); + // 2. 게시글 생성 + TipPost post = TipPost.builder() + .writer(member) + .title(dto.getTitle()) + .content(dto.getContent()) + .createdAt(LocalDateTime.now()) + .type(Post.Tip) + .accountStatus(AccountStatus.ACTIVE) + .reportCount(0) + .isAnonymous(dto.isAnonymous()) + .build(); + + // 3. 이미지 업로드 & 연관관계 매핑 (단일 파일만) + if (image != null && !image.isEmpty()) { + String imageUrl = s3UploadService.saveFile(image); + TipPostImage imageEntity = TipPostImage.builder() + .imageUrl(imageUrl) + .build(); + post.addImage(imageEntity); // 기존 이미지 clear 후 하나만 저장 } + tipPostRepository.save(post); + List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); - // [ 게시글 수정 ] : 작성자만 - @Transactional - public TipPostRespDto update(Long id, TipPostUpdateDto dto, String email, Long orgId, MultipartFile image) throws IOException { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - - TipPost post = tipPostRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); - - if (!post.getWriter().getId().equals(member.getId())) { - throw new AccessDeniedException("작성자만 수정할 수 있습니다."); - } + boolean anonymous = post.isAnonymous(); - // 기본 정보 수정 - post.update(dto.getTitle(), dto.getContent()); + // 응답 DTO 구성 (imageUrl 하나만) + return TipPostRespDto.builder() + .id(post.getId()) + .writer(anonymous ? "익명" : member.getNickname()) + .profileImageUrl(anonymous ? null : member.getProfileImageUrl()) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tagNames) + .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) + .isAnonymous(anonymous) + .isScrap(false) + .build(); + } - // 이미지 수정 로직 - String currentImageUrl = post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl(); - // 1) 삭제 요청 - if (Boolean.TRUE.equals(dto.getRemoveImage())) { - if (currentImageUrl != null) { - s3UploadService.deleteImage(currentImageUrl); - } - post.clearImages(); - } + // [ 게시글 수정 ] : 작성자만 + @Transactional + public TipPostRespDto update(Long id, TipPostUpdateDto dto, String email, Long orgId, + MultipartFile image) throws IOException { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - // 2) 새 이미지 업로드 (교체 or 추가) - if (image != null && !image.isEmpty()) { - if (currentImageUrl != null) { - s3UploadService.deleteImage(currentImageUrl); - post.clearImages(); - } - String newImageUrl = s3UploadService.saveFile(image); - TipPostImage newImage = TipPostImage.builder() - .imageUrl(newImageUrl) - .build(); - post.addImage(newImage); - } + TipPost post = tipPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); - // 태그 다시 매핑 - tagService.deleteTagsByPost(post); - List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); - - boolean anonymous = post.isAnonymous(); - - return TipPostRespDto.builder() - .id(post.getId()) - .writer(anonymous ? "익명" : post.getWriter().getNickname()) - .profileImageUrl(anonymous ? null : post.getWriter().getProfileImageUrl()) - .title(post.getTitle()) - .content(post.getContent()) - .createdAt(post.getCreatedAt()) - .tags(tagNames) - .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) - .isAnonymous(anonymous) - .build(); + if (!post.getWriter().getId().equals(member.getId())) { + throw new AccessDeniedException("작성자만 수정할 수 있습니다."); } + // 기본 정보 수정 + post.update(dto.getTitle(), dto.getContent()); - // [ 게시글 삭제 ] - @Transactional - public void deletePost(Long id, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + // 이미지 수정 로직 + String currentImageUrl = + post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl(); - TipPost post = tipPostRepository.findById(id) - .orElseThrow( () -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); - - // 권한 체크 : 요청 유저가 작성자인지 - if(!post.getWriter().getId().equals(member.getId())) { - throw new AccessDeniedException("해당 게시글을 수정할 권한이 없습니다."); - } - post.delete(); + // 1) 삭제 요청 + if (Boolean.TRUE.equals(dto.getRemoveImage())) { + if (currentImageUrl != null) { + s3UploadService.deleteImage(currentImageUrl); + } + post.clearImages(); } - // [ 게시글 스크랩 ] - @Transactional - public TipScrapRespDto toggleScrap(Long id, String email, Long orgId) { - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + // 2) 새 이미지 업로드 (교체 or 추가) + if (image != null && !image.isEmpty()) { + if (currentImageUrl != null) { + s3UploadService.deleteImage(currentImageUrl); + post.clearImages(); + } + String newImageUrl = s3UploadService.saveFile(image); + TipPostImage newImage = TipPostImage.builder() + .imageUrl(newImageUrl) + .build(); + post.addImage(newImage); + } - TipPost post = tipPostRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); + // 태그 다시 매핑 + tagService.deleteTagsByPost(post); + List tagNames = tagService.assignTagsToPost(post, dto.getTagIds()); + + boolean anonymous = post.isAnonymous(); + + return TipPostRespDto.builder() + .id(post.getId()) + .writer(anonymous ? "익명" : post.getWriter().getNickname()) + .profileImageUrl(anonymous ? null : post.getWriter().getProfileImageUrl()) + .title(post.getTitle()) + .content(post.getContent()) + .createdAt(post.getCreatedAt()) + .tags(tagNames) + .imageUrl(post.getImages().isEmpty() ? null : post.getImages().get(0).getImageUrl()) + .isAnonymous(anonymous) + .build(); + } + + + // [ 게시글 삭제 ] + @Transactional + public void deletePost(Long id, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + + TipPost post = tipPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + + // 권한 체크 : 요청 유저가 작성자인지 + if (!post.getWriter().getId().equals(member.getId())) { + throw new AccessDeniedException("해당 게시글을 수정할 권한이 없습니다."); + } + post.delete(); + } - if(!post.getWriter().getId().equals(orgId)) { - throw new AccessDeniedException("해당 조직 게시글이 아닙니다."); - } + // [ 게시글 스크랩 ] + @Transactional + public TipScrapRespDto toggleScrap(Long id, String email, Long orgId) { + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - Optional exisiting = tipScrapRepository.findByMemberOrgAndTipPost(member, post); + TipPost post = tipPostRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")); - if (exisiting.isPresent()) { - tipScrapRepository.delete(exisiting.get()); - post.decreaseScrapCount(); - return new TipScrapRespDto(false, null); - } - TipScrap scrap = TipScrap.builder() - .memberOrg(member) - .tipPost(post) - .savedAt(LocalDateTime.now()) - .build(); - - tipScrapRepository.save(scrap); - post.increaseScrapCount(); - - // 알림 전송 - if(!post.getWriter().getId().equals(member.getId())) { - - // 알림 엔티티 생성 - Notification notification = Notification.builder() - .member(post.getWriter().getMember()) - .memberOrgId(post.getWriter().getId()) - .type(NotificationType.SCRAP) - .message(member.getNickname() + "님이 글을 스크랩했습니다.") - .fromMemberOrgId(member.getId()) - .relatedUrl("/api/tip-posts/" + post.getId()) - .build(); - - //3. 알림 저장 및 전송을 위해 NotificationService 호출 - notificationService.send(notification); - } - return new TipScrapRespDto(true, scrap.getSavedAt()); + if (!post.getWriter().getId().equals(orgId)) { + throw new AccessDeniedException("해당 조직 게시글이 아닙니다."); } + Optional exisiting = tipScrapRepository.findByMemberOrgAndTipPost(member, post); - // [ 게시글 신고 ] - @Transactional - public String report(Long postId, String email, Long orgId) { - TipPost post = tipPostRepository.findById(postId) - .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); - - MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) - .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + if (exisiting.isPresent()) { + tipScrapRepository.delete(exisiting.get()); + post.decreaseScrapCount(); + return new TipScrapRespDto(false, null); + } + TipScrap scrap = TipScrap.builder() + .memberOrg(member) + .tipPost(post) + .savedAt(LocalDateTime.now()) + .build(); + + tipScrapRepository.save(scrap); + post.increaseScrapCount(); + + // 알림 전송 + if (!post.getWriter().getId().equals(member.getId())) { + + // 알림 엔티티 생성 + Notification notification = Notification.builder() + .member(post.getWriter().getMember()) + .memberOrgId(post.getWriter().getId()) + .type(NotificationType.SCRAP) + .message(member.getNickname() + "님이 글을 스크랩했습니다.") + .fromMemberOrgId(member.getId()) + .relatedUrl("/api/tip-posts/" + post.getId()) + .build(); + + //3. 알림 저장 및 전송을 위해 NotificationService 호출 + notificationService.send(notification); + } + return new TipScrapRespDto(true, scrap.getSavedAt()); + } - boolean alreadyReported = tipPostReportRepository.existsByReporterAndTipPost(member, post); - if (alreadyReported) { - return "이미 신고한 게시글입니다."; - } - tipPostReportRepository.save( - TipReport.builder() - .reporter(member) - .tipPost(post) - .build() - ); - // 신고 횟수 증가 - post.increaseReportCount(); - return "게시글을 신고하였습니다."; - } + // [ 게시글 신고 ] + @Transactional + public String report(Long postId, String email, Long orgId) { + TipPost post = tipPostRepository.findById(postId) + .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); - // 인기 3개 - @Transactional - public List getPopularPosts(Long orgId) { - List popularPosts = tipPostRepository.findTop3ByWriterIdOrderByViewCountDescScrapCountDesc(orgId); + MemberOrg member = memberOrgRepository.findByMemberEmailAndId(email, orgId) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - return popularPosts.stream() - .map(TipPopularPostRespDto::from) - .toList(); + boolean alreadyReported = tipPostReportRepository.existsByReporterAndTipPost(member, post); + if (alreadyReported) { + return "이미 신고한 게시글입니다."; } + tipPostReportRepository.save( + TipReport.builder() + .reporter(member) + .tipPost(post) + .build() + ); + // 신고 횟수 증가 + post.increaseReportCount(); + return "게시글을 신고하였습니다."; + } + + // 인기 3개 + @Transactional + public List getPopularPosts(Long orgId) { + List popularPosts = tipPostRepository.findTop3ByWriterIdOrderByViewCountDescScrapCountDesc( + orgId); + + return popularPosts.stream() + .map(TipPopularPostRespDto::from) + .toList(); + + } } diff --git a/src/main/java/org/example/tackit/global/response/ApiResponse.java b/src/main/java/org/example/tackit/global/response/ApiResponse.java new file mode 100644 index 0000000..f288595 --- /dev/null +++ b/src/main/java/org/example/tackit/global/response/ApiResponse.java @@ -0,0 +1,52 @@ +package org.example.tackit.global.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponse { + + private Status status; + private T content; + + /** + * 성공 (데이터 있음) 예: ApiResponse.success(HttpStatus.OK, "조회 성공", data) + */ + public static ResponseEntity> success(HttpStatus httpStatus, String message, + T content) { + return ResponseEntity + .status(httpStatus) + .body(new ApiResponse<>(new Status(httpStatus.value(), message), content)); + } + + /** + * 성공 (데이터 없음) 예: ApiResponse.success(HttpStatus.OK, "삭제 성공") + */ + public static ResponseEntity> success(HttpStatus httpStatus, String message) { + return ResponseEntity + .status(httpStatus) + .body(new ApiResponse<>(new Status(httpStatus.value(), message), null)); + } + + /** + * 실패 (에러 메시지만 반환) 예: ApiResponse.fail(HttpStatus.BAD_REQUEST, "잘못된 요청입니다") + * TODO 추후 exception 쪽에서 사용하도록 수정해야함 + */ + public static ResponseEntity> fail(HttpStatus httpStatus, String message) { + return ResponseEntity + .status(httpStatus) + .body(new ApiResponse<>(new Status(httpStatus.value(), message), null)); + } + + @Getter + @AllArgsConstructor + public static class Status { + + private int code; + private String message; + } +} \ No newline at end of file