2024. 5. 22. 01:03ㆍProject/Naver Cafe
네이버 카페는 네이버 회원 계정만 있다면 손 쉽게 만들 수 있다. 네이버 카페가 지원하는 '카페 만들기' 서비스는 다음과 같은 흐름으로 진행된다.
- 카페 생성에 필요한 정보 입력
- 카페 생성 회원은 자동적으로 생성한 카페의 매니저(관리자)로 가입 진행
위 내용처럼 카페 생성은 물론이고 생성한 회원의 카페 가입까지 자동적으로 이루어져야 한다.
Cafe
package CloneCoding.NaverCafe.domain.cafe;
@Entity
@Table(name = "CAFE")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Cafe {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "NAME", unique = true)
private String name;
@Column(name = "URL", unique = true)
private String url;
@Column(name = "ICON")
private String icon;
@Column(name = "PRIVACY_JOIN_SETTING")
private String privacyJoinSetting;
@Column(name = "USE_REAL_NAME")
private boolean useRealName;
@Column(name = "OPEN_MEMBER_LIST")
private boolean openMemberList;
@Column(name = "MAIN_CATEGORY")
private String mainCategory;
@Column(name = "SUB_CATEGORY")
private String subCategory;
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "ACTIVITY_AREA")
private String activityArea;
@OneToMany(mappedBy = "cafeId", fetch = FetchType.LAZY)
private List<Keyword> keywords = new ArrayList<>();
@OneToMany(mappedBy = "cafeId", fetch = FetchType.LAZY)
private List<CafeMember> members = new ArrayList<>();
public static Cafe createCafe(RequestCreateCafe.CafeInfo cafeInfo) {
PrivacyJoinSetting setting = PrivacyJoinSetting.findByOption(cafeInfo.getPrivacyJoinSetting());
return Cafe.builder()
.name(cafeInfo.getName())
.url(BASIC_URL.getUrl() + cafeInfo.getUrl())
.icon(cafeInfo.getIcon())
.privacyJoinSetting(setting.name())
.useRealName(cafeInfo.isUseRealName())
.openMemberList(cafeInfo.isOpenMemberList())
.mainCategory(cafeInfo.getMainCategory())
.subCategory(cafeInfo.getSubCategory())
.description(cafeInfo.getDescription())
.activityArea(cafeInfo.getActivityArea())
.build();
}
}
- createCafe(RequestCreateCafe.CafeInfo cafeInfo) : 입력 받은 정보로 Cafe 객체를 생성하고 객체를 반환
BasicURL
package CloneCoding.NaverCafe.domain.cafe;
@Getter
@RequiredArgsConstructor
public enum BasicURL {
BASIC_URL("https://cafe.naver.com/")
;
private final String url;
}
네이버 카페 주소는 생성시 기본 URL에 사용자가 지정한 URL이 더해져서 만들어진다. 해당 Enum 클래스는 기본 URL에 상수로 해당 정보를 지정하는데 사용하였다.
PrivacyJoinSetting
package CloneCoding.NaverCafe.domain.cafe;
@Getter
@RequiredArgsConstructor
public enum PrivacyJoinSetting {
SIGN_UP_NOW("바로 가입"),
SIGN_UP_APPROVED("가입 승인"),
ACCEPT_INVITATION("초대 승인")
;
private final String option;
public static PrivacyJoinSetting findByOption(String option) {
for (PrivacyJoinSetting setting : values()) {
if (setting.getOption().equals(option)) {
return setting;
}
}
throw new RuntimeException(new NoSuchElementException("설정 정보를 찾을 수 없습니다."));
}
}
가입 설정에 대한 정보를 상수로 갖는 Enum 클래스이다.
- findByOption(String option) : 옵션 정보를 통해 해당 클래스에서 옵션 정보와 일치하는 상수를 찾아 반환
RequestCreateCafe
package CloneCoding.NaverCafe.domain.cafe.dto;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RequestCreateCafe {
private CafeInfo cafeInfo;
private List<String> keywords = new ArrayList<>();
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class CafeInfo {
@NotEmpty
@Size(min = 1, max = 60,
message = "카페 이름은 1 ~ 60자로 제한됩니다.")
private String name;
@NotNull
@Size(min = 1, max = 20,
message = "카페 주소는 1 ~ 20자로 제한됩니다.")
@Pattern(regexp = "^[a-zA-Z0-9]+",
message = "카페 주소는 영소문자, 영대문자, 숫자만 사용 가능합니다.")
private String url;
private String icon = "basic_img";
@NotNull
private String privacyJoinSetting;
@NotNull
private boolean useRealName;
@NotNull
private boolean openMemberList;
@NotNull
private String mainCategory;
@NotNull
private String subCategory;
@NotNull
private String description;
private String activityArea = "없음";
}
}
카페를 만들 때 입력받은 정보를 전달하는 DTO이다. 입력 정보를 크게 cafeInfo와 keywords, 두 개로 나누었는데 이유는 cafeInfo는 Cafe 객체 생성에 이용되고 keywords는 Keyword 객체 생성에 사용하기 위함이다. 서로 다른 객체를 생성하기에 입력정보를 분리하여 사용할 수 있도록 나누었다.
CafeController
package CloneCoding.NaverCafe.domain.cafe.controller;
@Slf4j
@RestController
@RequiredArgsConstructor
@Transactional
@RequestMapping("/cafe")
public class CafeController {
private final CafeService cafeService;
private final KeywordService keywordService;
private final CafeMemberService cafeMemberService;
@PostMapping("/create")
public String createCafe(@RequestBody @Valid RequestCreateCafe request,
@RequestHeader("Authorization") String token) {
log.info("카페 생성 요청");
Cafe madeCafe = cafeService.createCafe(request.getCafeInfo());
if (!request.getKeywords().isEmpty()) {
keywordService.applyKeywords(request.getKeywords(), madeCafe);
}
cafeMemberService.addCafeManager(token, madeCafe);
return CREATE_CAFE_COMPLETE.getMessage();
}
}
카페 생성 요청 API의 정보를 처리하는 컨트롤러이다. 카페 생성뿐아니라 키워드 저장, 사용자 자동 가입이 되어야 하기에 세 가지 서비스 인터페이스를 주입받게 되었다. 클래스에 @Transactionald을 적용해 클래스 내 메서드들은 모든 과정이 정상이어야 최종적으로 DB에 반영된다.
- createCafe(RequestCreateCafe request, String token) : Cafe 객체 생성, Keyword 객체(들) 생성, 사용자 카페 매니저 등급으로 자동 가입하는 모든 과정이 끝나면 이를 DB에 반영 후 결과 메시지를 반환
CafeServiceImpl
package CloneCoding.NaverCafe.domain.cafe.service;
@Service
@RequiredArgsConstructor
public class CafeServiceImpl implements CafeService {
private final CafeRepository cafeRepository;
@Override
public Cafe createCafe(RequestCreateCafe.CafeInfo request) {
Cafe cafe = Cafe.createCafe(request);
return cafeRepository.save(cafe);
}
}
CafeService 인터페이스를 구현한 구현 클래스이다.
- createCafe(RequestCreateCafe.CafeInfo request) : 요청 정보를 통해 Cafe 객체를 생성하고 엔티티를 저장한다.
Keyword
package CloneCoding.NaverCafe.domain.keyword;
@Entity
@Table(name = "KEYWORD")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Keyword {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "KEYWORD")
private String keyword;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CAFE_ID")
private Cafe cafeId;
public static Keyword addKeyword(String keyword, Cafe cafe) {
return Keyword.builder()
.keyword(keyword)
.cafeId(cafe)
.build();
}
}
- addKeyword(String keyword, Cafe cafe) : keyword와 카페 id를 통해 Keyword 객체를 생성한 후 반환
KeywordServiceImpl
package CloneCoding.NaverCafe.domain.keyword.service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class KeywordServiceImpl implements KeywordService {
private final KeywordRepository keywordRepository;
@Override
public void applyKeywords(List<String> keywords, Cafe cafe) {
for (String keyword : keywords) {
Keyword addKeyword = Keyword.addKeyword(keyword, cafe);
keywordRepository.save(addKeyword);
}
}
}
- applyKeywords(List<String> keywords, Cafe cafe) : 입력한 키워드들로 Keyword 객체들을 생성하고 엔티티 저장
CafeMember
package CloneCoding.NaverCafe.domain.cafeMember;
@Entity
@Table(name = "CAFE_MEMBER")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class CafeMember {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "ACCOUNT_ID", unique = true)
private String accountId;
@Column(name = "NICKNAME", unique = true)
private String nickname;
@Column(name = "PROFILE_IMAGE")
@Builder.Default
private String profileImage = "default_image";
@Column(name = "DESCRIPTION")
@Builder.Default
private String description = "자기소개를 입력해주세요";
@Column(name = "OPEN_SETTING")
@Builder.Default
private boolean openSetting = true;
@Column(name = "GENDER")
private String gender;
@Column(name = "BIRTHDAY")
private LocalDate birthday;
@Column(name = "POSITION")
@Builder.Default
private String position = CAFE_MEMBER.name();
@Column(name = "GRADE")
@Builder.Default
private String grade = "-";
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CAFE_ID")
private Cafe cafeId;
public static CafeMember addCafeManager(String accountId, String nickname,
String gender, LocalDate birthday,
Cafe cafe) {
return CafeMember.builder()
.accountId(accountId)
.nickname(nickname)
.gender(gender)
.birthday(birthday)
.position(MANAGER.name())
.grade(MANAGER.getGrade())
.cafeId(cafe)
.build();
}
}
- addCafeManager(String accountId, String nickname, String gender, LocalDate birthday, Cafe cafe) : 매개변수의 정보로 카페 매니저를 추가(가입)
CafeManagerPosition
package CloneCoding.NaverCafe.domain.cafeMember;
@Getter
@RequiredArgsConstructor
public enum CafeMemberPosition {
MANAGER("매니저", "카페 매니저", "카페 매니저"),
SUB_MANAGER("스탭", "부 매니저", "부 매니저"),
WELCOME_STAFF("스탭", "카페 스탭", "신입 맞이 스탭"),
DESIGN_STAFF("스탭", "카페 스탭", "디자인 스탭"),
EVENT_STAFF("스탭", "카페 스탭", "이벤트 스탭"),
ENTIRE_BULLETIN_BOARD_STAFF("스탭", "카페 스탭", "전체 게시판 스탭"),
EACH_BULLETIN_BOARD_STAFF("스탭", "카페 스탭", "개별 게시판 스탭"),
MEMBERSHIP_STAFF("스탭", "카페 스탭", "멤버등급 스탭"),
GROUP_BUYING_STAFF("스탭", "카페 스탭", "공동구매 스탭"),
CAFE_MEMBER("회원", "카페 멤버", "일반 멤버")
;
private final String grade;
private final String position;
private final String detailPosition;
}
카페 멤버의 역할을 상수로 갖는 Enum 클래스이다. 각 상수는 등급(grade), 역할(position), 세부역할(detailPosition)에 대한 값을 갖는다. 등급의 경우 카페 등급 아이콘을 기준으로 나누었으며, 역할은 등급명, 세부역할은 스탭 지정 정보를 통해 나누었다.
CafeMemberServiceImpl
package CloneCoding.NaverCafe.domain.cafeMember.service;
import CloneCoding.NaverCafe.domain.cafe.Cafe;
import CloneCoding.NaverCafe.domain.cafeMember.CafeMember;
import CloneCoding.NaverCafe.domain.cafeMember.repository.CafeMemberRepository;
import CloneCoding.NaverCafe.domain.member.Member;
import CloneCoding.NaverCafe.domain.member.repository.MemberRepository;
import CloneCoding.NaverCafe.security.AesUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CafeMemberServiceImpl implements CafeMemberService {
private final CafeMemberRepository cafeMemberRepository;
private final MemberRepository memberRepository;
private final AesUtil aesUtil;
@Override
public void addCafeManager(String token, Cafe cafe) {
String accountId = aesUtil.aesDecode(token);
Member findMember = memberRepository.findByAccountId(accountId);
CafeMember cafeManager = CafeMember.addCafeManager(
findMember.getAccountId(), findMember.getNickname(),
findMember.getGender(), findMember.getBirthday(), cafe);
cafeMemberRepository.save(cafeManager);
}
}
- addCafeManager(String token, Cafe cafe) : 토큰과 카페 id를 통해 CafeMember 객체를 생성 후 엔티티 저장
API TEST
API 요청에 카페 가입 정보를 담았을 때 정상적으로 카페가 생성된 것을 확인 할 수 있었다.
API 요청 완료 후, DB를 확인한 내용이다. 정말 아쉽게도 원하는대로 모든 요청이 정상일 때만 최종적으로 모든 테이블에 데이터가 저장되지만 문제는 이미 서비스 계층마다 JpaRepository의 save()가 수행되었기 때문에 CAFE 테이블의 ID가 1이 아니며, KEYWORD 테이블의 ID가 1,2가 아닌 것을 확인 할 수 있다. 만약 원하는대로 ID 값을 갖게하기 위해서는 컨트롤러에서 다른 도메인의 서비스를 다루면 안 될 것 같은데 이 부분은 추후에 수정해야 할 것이다.
수정사항(24.05.22)
API TEST 이 후 Cafe 엔티티의 id 값이 낭비되는 현상에 대해 수정이 필요하다 느꼈고 해당 부분을 수정하였다.
고치고자 하는 상세한 내용은 컨트롤러에서 여러 서비스를 주입받아 사용하기에 각 서비스별로 JpaRepository의 save()를 사용해 DB의 반영이 따로 이루어져 먼저 호출된 서비스는 이미 DB에 데이터가 반영됬기 때문에 이후 롤백이 되더라도 이미 id는 한 번 증가해 값의 낭비가 생기는거라 판단했다.
문제 해결을 위해서 우선 컨트롤러가 아닌 서비스(CafeServiceImpl)에서 모든 기능을 수행 후 마지막에 각 레포지토리를 통해 DB에 데이터를 반영하도록 코드들을 수정하였다.
package CloneCoding.NaverCafe.domain.cafe.service;
@Service
@RequiredArgsConstructor
public class CafeServiceImpl implements CafeService {
private final CafeRepository cafeRepository;
private final MemberRepository memberRepository;
private final KeywordRepository keywordRepository;
private final CafeMemberRepository cafeMemberRepository;
private final AesUtil aesUtil;
@Override
public String createCafe(RequestCreateCafe request, String token) {
Cafe cafe = Cafe.createCafe(request.getCafeInfo());
String accountId = aesUtil.aesDecode(token);
Member findMember = memberRepository.findByAccountId(accountId);
List<Keyword> keywords = createKeywords(request.getKeywords(), cafe);
CafeMember cafeManager = createCafeManager(
findMember.getAccountId(), findMember.getNickname(),
findMember.getGender(), findMember.getBirthday(), cafe
);
cafeRepository.save(cafe);
keywordRepository.saveAll(keywords);
cafeMemberRepository.save(cafeManager);
return SystemMessage.CREATE_CAFE_COMPLETE.getMessage();
}
private static List<Keyword> createKeywords(List<String> keywords, Cafe cafe) {
List<Keyword> result = new ArrayList<>();
if (!keywords.isEmpty()) {
result = Keyword.createKeywords(keywords, cafe);
}
return result;
}
private static CafeMember createCafeManager(String accountId, String nickname,
String gender, LocalDate birthday, Cafe cafe) {
return CafeMember.addCafeManager(accountId, nickname, gender, birthday, cafe);
}
}
- createCafe() : 요청 정보를 통해 카페를 생성하고 시스템 메시지를 반환
- createKeywords() : 키워드, 카페 정보를 통해 Keyword 객체들을 생성하고 List<Keyword>에 담아 반환
- createCafeManager() : 카페 매니저(생성한 사용자) 정보를 통해 CafeMember 객체를 생성하고 반환
코드 수정 후 이전처럼 예외가 발생하도록 API 요청 후 정상 요청을 진행하니 이전에 문제가 되었던 식별키(id) 낭비 현상이 고쳐졌다. 이제는 createCafe()를 수행하는 도중 예외가 발생해도 정보가 DB에 반영되지 않았기 때문이라 생각한다.
수정사항(24.05.26)
단순히 기능을 추가한다고 빼먹은 내용이 있다. 바로 카페 생성 양식에 필요한 데이터를 반환하는 기능이다. 한 마디로 카페 만들기 버튼을 누르면 생성에 필요한 정보를 입력하는 부분이 사용자에게 제공되어야 하고 그 때 필요한 데이터를 제공하는 기능이 필요한데 그 부분을 생략하였다. 이렇게 되니 생성 기능을 구현시 어떠한 데이터가 서버에 전달될지 예상하고 이를 코드에 적용하는 것이 애매해졌고 해당 부분의 구현이 필요하다는 걸 깨달았다.
결국 카페 생성 양식에 필요한 데이터를 반환하는 기능을 구현하였고 더 이상 어떤 데이터를 사용자에게 전달 받게 되고 어떤 데이터만을 수용해 DB에 반영할지 고민하지 않게 되었다.
'Project > Naver Cafe' 카테고리의 다른 글
[클론 코딩] 네이버 카페 - 카페 회원정보 수정 (0) | 2024.05.26 |
---|---|
[클론 코딩] 네이버 카페 - 카페 회원 가입 (0) | 2024.05.22 |
[클론 코딩] 네이버 카페 - 메모 (0) | 2024.05.12 |
[클론 코딩] 네이버 카페 - 네이버 회원 탈퇴 (0) | 2024.05.12 |
[클론 코딩] 네이버 카페 - 네이버 회원 정보 수정 (0) | 2024.05.12 |