diff --git a/src/main/java/com/example/ReMap/domain/auth/entity/RefreshToken.java b/src/main/java/com/example/ReMap/domain/auth/entity/RefreshToken.java index e4ac48f..9517ca0 100644 --- a/src/main/java/com/example/ReMap/domain/auth/entity/RefreshToken.java +++ b/src/main/java/com/example/ReMap/domain/auth/entity/RefreshToken.java @@ -1,77 +1,45 @@ package com.example.ReMap.domain.auth.entity; - import com.example.ReMap.domain.member.Member; import jakarta.persistence.*; +import lombok.*; import java.time.LocalDateTime; - @Entity @Table(name = "refresh_token") +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder public class RefreshToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false) + // Member 삭제 시 토큰도 함께 삭제 + @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE) + @JoinColumn(name = "member_id", nullable = false, unique = true) private Member member; - @Column(name = "refresh_token", nullable = false) + @Column(name = "refresh_token", nullable = false, length = 512) private String refreshToken; - @Column(name = "created_at") + @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; - @Column(name = "updated_at") + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; - // 기본 생성자 - public RefreshToken() {} - - // 생성자 - public RefreshToken(Member member, String refreshToken, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.member = member; - this.refreshToken = refreshToken; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - // Getter / Setter - public Long getId() { - return id; - } - - public Member getMember() { - return member; - } - - public void setMember(Member member) { - this.member = member; - } - - public String getRefreshToken() { - return refreshToken; - } - - public void setRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; + @PrePersist + public void prePersist() { + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; + this.updatedAt = now; } - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/example/ReMap/domain/cast/Cast.java b/src/main/java/com/example/ReMap/domain/cast/Cast.java index 9d26661..cbc9608 100644 --- a/src/main/java/com/example/ReMap/domain/cast/Cast.java +++ b/src/main/java/com/example/ReMap/domain/cast/Cast.java @@ -1,6 +1,7 @@ package com.example.ReMap.domain.cast; import com.example.ReMap.domain.member.Member; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -20,6 +21,7 @@ public class Cast { @ManyToOne @JoinColumn(name = "member_id") + @JsonIgnore private Member member; // 생성자, getter, setter diff --git a/src/main/java/com/example/ReMap/domain/comment/Comment.java b/src/main/java/com/example/ReMap/domain/comment/Comment.java index fbe0ae1..8a11c41 100644 --- a/src/main/java/com/example/ReMap/domain/comment/Comment.java +++ b/src/main/java/com/example/ReMap/domain/comment/Comment.java @@ -2,6 +2,7 @@ import com.example.ReMap.domain.cast.Cast; import com.example.ReMap.domain.member.Member; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -19,6 +20,7 @@ public class Comment { @ManyToOne @JoinColumn(name = "member_id") + @JsonIgnore private Member member; @ManyToOne diff --git a/src/main/java/com/example/ReMap/domain/member/ChangeMemberTypeRequest.java b/src/main/java/com/example/ReMap/domain/member/ChangeMemberTypeRequest.java new file mode 100644 index 0000000..d694584 --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/ChangeMemberTypeRequest.java @@ -0,0 +1,17 @@ +package com.example.ReMap.domain.member; + +import jakarta.validation.constraints.NotNull; + +public class ChangeMemberTypeRequest { + + @NotNull(message = "회원 유형을 선택해주세요.") + private MemberType memberType; + + public MemberType getMemberType() { + return memberType; + } + + public void setMemberType(MemberType memberType) { + this.memberType = memberType; + } +} diff --git a/src/main/java/com/example/ReMap/domain/member/ChangePasswordRequest.java b/src/main/java/com/example/ReMap/domain/member/ChangePasswordRequest.java new file mode 100644 index 0000000..fb54dd5 --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/ChangePasswordRequest.java @@ -0,0 +1,15 @@ +package com.example.ReMap.domain.member; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class ChangePasswordRequest { + + @NotBlank(message = "현재 비밀번호를 입력해주세요.") + private String currentPassword; + + @NotBlank(message = "새 비밀번호를 입력해주세요.") + private String newPassword; +} diff --git a/src/main/java/com/example/ReMap/domain/member/Member.java b/src/main/java/com/example/ReMap/domain/member/Member.java index 7e4f49e..8a88908 100644 --- a/src/main/java/com/example/ReMap/domain/member/Member.java +++ b/src/main/java/com/example/ReMap/domain/member/Member.java @@ -2,32 +2,25 @@ import com.example.ReMap.domain.cast.Cast; import com.example.ReMap.domain.comment.Comment; -import com.example.ReMap.domain.auth.entity.RefreshToken; -import com.example.ReMap.domain.member.MemberType; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Entity @Getter -@Builder -@AllArgsConstructor +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder @Table(name = "member") public class Member { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private String password; - - private Integer phone; - private Long birth; @Column(unique = true) @@ -36,24 +29,20 @@ public class Member { @Column(nullable = false, unique = true) private String nickname; - @Column(name = "inactive_date") - private LocalDateTime inactiveDate; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - + @Builder.Default @Enumerated(EnumType.STRING) - @Column(name = "memberType") - private MemberType memberType = MemberType.GENERAL; + @Column(name = "member_type") + private MemberType memberType = MemberType.USER; - // 관계 매핑 + // 내가 올린 오디오 @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @Builder.Default private List casts = new ArrayList<>(); + // 내가 쓴 댓글 @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @Builder.Default private List comments = new ArrayList<>(); - - } +} diff --git a/src/main/java/com/example/ReMap/domain/member/MemberController.java b/src/main/java/com/example/ReMap/domain/member/MemberController.java new file mode 100644 index 0000000..6703c8a --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/MemberController.java @@ -0,0 +1,74 @@ +package com.example.ReMap.domain.member; + +import com.example.ReMap.domain.cast.Cast; +import com.example.ReMap.domain.comment.Comment; +import com.example.ReMap.domain.auth.util.JwtUtil; // 실제 JwtUtil 위치에 맞춰 수정 +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/members") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + private final JwtUtil jwtUtil; + + /** 내 정보 조회 **/ + @GetMapping("/setting") + public ResponseEntity getMember( + @RequestHeader("Authorization") String authorization) { + Long memberId = jwtUtil.extractMemberId(authorization); + return ResponseEntity.ok(memberService.getMemberById(memberId)); + } + + /** 내 정보 수정 **/ + @PutMapping("/update") + public ResponseEntity updateMember( + @RequestHeader("Authorization") String authorization, + @Valid @RequestBody UpdateMemberRequest req) { + Long memberId = jwtUtil.extractMemberId(authorization); + return ResponseEntity.ok(memberService.updateMember(memberId, req)); + } + + /** 비밀번호 변경 **/ + @PutMapping("/password") + public ResponseEntity changePassword( + @RequestHeader("Authorization") String authorization, + @Valid @RequestBody ChangePasswordRequest req) { + Long memberId = jwtUtil.extractMemberId(authorization); + memberService.changePassword(memberId, req); + return ResponseEntity.ok().build(); + } + + /** 내가 쓴 댓글 목록 **/ + @GetMapping("/comments") + public ResponseEntity> getComments( + @RequestHeader("Authorization") String authorization) { + Long memberId = jwtUtil.extractMemberId(authorization); + return ResponseEntity.ok(memberService.getMemberComments(memberId)); + } + + /** 내가 올린 오디오 목록 **/ + @GetMapping("/casts") + public ResponseEntity> getCasts( + @RequestHeader("Authorization") String authorization) { + Long memberId = jwtUtil.extractMemberId(authorization); + return ResponseEntity.ok(memberService.getMemberCasts(memberId)); + } + + /** USER ↔ RECORDER 역할 전환 **/ + @PutMapping("/member-type") + public ResponseEntity changeRole( + @RequestHeader("Authorization") String authorization, + @Valid @RequestBody ChangeMemberTypeRequest req) { + Long memberId = jwtUtil.extractMemberId(authorization); + return ResponseEntity.ok( + memberService.changeOwnRole(memberId, req.getMemberType()) + ); + } +} diff --git a/src/main/java/com/example/ReMap/domain/member/MemberRepository.java b/src/main/java/com/example/ReMap/domain/member/MemberRepository.java new file mode 100644 index 0000000..39cec94 --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/MemberRepository.java @@ -0,0 +1,10 @@ +package com.example.ReMap.domain.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + boolean existsByNickname(String nickname); +} + diff --git a/src/main/java/com/example/ReMap/domain/member/MemberService.java b/src/main/java/com/example/ReMap/domain/member/MemberService.java new file mode 100644 index 0000000..a90f14d --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/MemberService.java @@ -0,0 +1,78 @@ +package com.example.ReMap.domain.member; + +import com.example.ReMap.domain.cast.Cast; +import com.example.ReMap.domain.comment.Comment; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final MemberRepository memberRepository; + + /* -------------------------- Read (조회) ----------------------------- */ + + @Transactional(readOnly = true) + public Member getMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다. ID: " + id)); + } + + @Transactional(readOnly = true) + public List getMemberComments(Long memberId) { + return getMemberById(memberId).getComments(); + } + + @Transactional(readOnly = true) + public List getMemberCasts(Long memberId) { + Member member = getMemberById(memberId); + // USER도 자신의 캐스트(업로드)를 볼 수 있게 하려면 아래 검사를 제거하세요. + if (member.getMemberType() != MemberType.RECORDER && member.getMemberType() != MemberType.ADMIN) { + throw new IllegalArgumentException("기록자만 오디오 목록을 조회할 수 있습니다."); + } + return member.getCasts(); + } + + /* -------------------------- Update (수정) --------------------------- */ + + public Member updateMember(Long id, UpdateMemberRequest dto) { + Member member = getMemberById(id); + + // 닉네임 변경 시 중복 검사 + if (dto.getNickname() != null && !dto.getNickname().equals(member.getNickname()) + && memberRepository.existsByNickname(dto.getNickname())) { + throw new IllegalArgumentException("이미 사용 중인 닉네임입니다."); + } + member.setNickname(dto.getNickname()); + + // 생년월일 업데이트 + member.setBirth(dto.getBirth()); + + return member; + } + + /* ----------------------- Role Self-Change -------------------------- */ + + /** + * 본인이 USER ↔ RECORDER 로만 스스로 타입 변경 + */ + public Member changeOwnRole(Long id, MemberType newType) { + Member member = getMemberById(id); + + if (newType != MemberType.USER && newType != MemberType.RECORDER) { + throw new IllegalArgumentException("변경 가능한 역할은 USER 또는 RECORDER 뿐입니다."); + } + + member.setMemberType(newType); + return member; + } + + + +} diff --git a/src/main/java/com/example/ReMap/domain/member/MemberType.java b/src/main/java/com/example/ReMap/domain/member/MemberType.java index 825c576..a9c7812 100644 --- a/src/main/java/com/example/ReMap/domain/member/MemberType.java +++ b/src/main/java/com/example/ReMap/domain/member/MemberType.java @@ -1,6 +1,17 @@ package com.example.ReMap.domain.member; public enum MemberType { - GENERAL, // 일반 유저 - RECORDER // 기록자 -} + USER("일반유저"), // 오디오 감상, 감상후기 작성 + RECORDER("기록자"), // 음성파일 업로드 + ADMIN("관리자"); // 전체 관리 + + private final String description; + + MemberType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/ReMap/domain/member/UpdateMemberRequest.java b/src/main/java/com/example/ReMap/domain/member/UpdateMemberRequest.java new file mode 100644 index 0000000..5d17d2c --- /dev/null +++ b/src/main/java/com/example/ReMap/domain/member/UpdateMemberRequest.java @@ -0,0 +1,18 @@ +package com.example.ReMap.domain.member; + +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UpdateMemberRequest { + + + @NotNull(message = "생년월일을 입력해 주세요.") + private Long birth; + + @NotBlank(message = "닉네임을 입력해 주세요.") + @Size(max = 50, message = "닉네임은 최대 50자까지 가능합니다.") + private String nickname; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 5bcf2fa..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,22 +0,0 @@ -spring.application.name=ReMap -# ?? ?? -server.port=8080 - -# MySQL ?? ?? -spring.datasource.url=jdbc:mysql://localhost:3306/hackathon?serverTimezone=Asia/Seoul -spring.datasource.username=root -spring.datasource.password=1234 -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - -# JPA ?? -spring.jpa.hibernate.ddl-auto=none -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect - -# H2 ?? (???) -spring.h2.console.enabled=true - -# ?? ?? (??) -logging.level.org.hibernate.SQL=debug -logging.level.org.hibernate.type=trace diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..6970400 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/hackathon?createDatabaseIfNotExist=TRUE&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: root + password: 1234 + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + show_sql: true + +springdoc: + swagger-ui: + path: /swagger-ui.html + operationsSorter: method + disable-swagger-default-url: true + display-request-duration: true + api-docs: + path: /api-docs + show-actuator: true + default-consumes-media-type: application/json + default-produces-media-type: application/json + paths-to-match: + - /api/** \ No newline at end of file