Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies {
implementation 'org.aspectj:aspectjweaver'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public record EmailSignupRequest(
@Email(message = "올바른 이메일 형식이 아닙니다.")
String email,

@NotBlank(message = "이름은 필수입니다.")
String nickname,

@NotBlank(message = "비밀번호는 필수입니다.")
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d).{8,}$",
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/JJIN/domain/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public class Member {
@Column(nullable = false)
private String email;

@Column
private String nickname;

@Column
private String password;

Expand All @@ -36,23 +39,27 @@ public class Member {

public static Member create(
final String email,
final String nickname,
final String password,
final Role role
) {
return Member.builder()
.email(email)
.nickname(nickname)
.password(password)
.role(role)
.build();
}

public static Member createSocialMember(
final String email,
final String nickname,
final String socialId,
final Role role
) {
return Member.builder()
.email(email)
.nickname(nickname)
.socialId(socialId)
.role(role)
.build();
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/com/JJIN/domain/member/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ public AuthTokenResponse signup(final EmailSignupRequest request) {
}

Member member = memberRepository.save(
Member.create(request.email(), passwordEncoder.encode(request.password()), Role.ONBOARDING)
Member.create(
request.email(),
request.nickname(),
passwordEncoder.encode(request.password()),
Role.ONBOARDING
)
);
termsService.saveAgreements(member.getId(), request.termsAgreements());
emailVerificationCodeStore.deleteVerified(request.email());
Expand Down Expand Up @@ -132,7 +137,7 @@ private AuthTokenResponse issueTokens(final Member member) {
private Member registerNewMember(final GoogleUserInfo userInfo) {
log.info("신규 구글 회원 가입: socialId={}, email={}", userInfo.sub(), userInfo.email());
return memberRepository.save(
Member.createSocialMember(userInfo.email(), userInfo.sub(), Role.ONBOARDING)
Member.createSocialMember(userInfo.email(), userInfo.name(), userInfo.sub(), Role.ONBOARDING)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.JJIN.domain.mission.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

/**
* '요즘 핫한 미션' 집계 설정.
*
* <ul>
* <li>windowDays: 집계 및 고정 기간(일). 이 기간 동안 핫 미션은 바뀌지 않는다.</li>
* <li>topN: 노출할 상위 미션 개수.</li>
* </ul>
* 순위는 기간 내 미션 추가(담기) 횟수로 매긴다.
*/
@ConfigurationProperties(prefix = "hot-mission")
public record HotMissionProperties(

@DefaultValue("4") int windowDays,

@DefaultValue("5") int topN
) {
}
14 changes: 14 additions & 0 deletions src/main/java/com/JJIN/domain/mission/config/SchedulingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.JJIN.domain.mission.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
* 핫 미션 재집계 스케줄러를 위한 스케줄링 활성화 및 설정 바인딩.
*/
@Configuration
@EnableScheduling
@EnableConfigurationProperties(HotMissionProperties.class)
public class SchedulingConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.JJIN.domain.mission.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.JJIN.domain.mission.controller.docs.MissionControllerDocs;
import com.JJIN.domain.mission.dto.response.HotMissionListResponse;
import com.JJIN.domain.mission.dto.response.MissionDetailResponse;
import com.JJIN.domain.mission.dto.response.MissionSearchFeedResponse;
import com.JJIN.domain.mission.entity.enums.MissionDifficulty;
import com.JJIN.domain.mission.exception.MissionSuccessCode;
import com.JJIN.domain.mission.service.HotMissionService;
import com.JJIN.domain.mission.service.MissionService;
import com.JJIN.domain.onboarding.entity.enums.TourApiContentType;
import com.JJIN.global.auth.annotation.CurrentMember;
import com.JJIN.global.auth.dto.CurrentAuth;
import com.JJIN.global.auth.jwt.exception.TokenErrorCode;
import com.JJIN.global.exception.JjinException;
import com.JJIN.global.response.dto.SuccessResponse;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/missions")
@RequiredArgsConstructor
public class MissionController implements MissionControllerDocs {

private final MissionService missionService;
private final HotMissionService hotMissionService;

@Override
@GetMapping
public ResponseEntity<SuccessResponse<MissionSearchFeedResponse>> searchMissions(
@CurrentMember CurrentAuth currentAuth,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) List<TourApiContentType> categories,
@RequestParam(required = false) List<MissionDifficulty> difficulties,
@RequestParam(defaultValue = "popular") String sort,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
validateAuthenticated(currentAuth);
MissionSearchFeedResponse response = missionService.searchMissions(
currentAuth.memberId(),
keyword,
categories,
difficulties,
sort,
page,
size
);
return ResponseEntity.ok(SuccessResponse.of(MissionSuccessCode.MISSION_SEARCH_SUCCESS, response));
}

@Override
@GetMapping("/{missionId}")
public ResponseEntity<SuccessResponse<MissionDetailResponse>> getMission(
@CurrentMember CurrentAuth currentAuth,
@PathVariable Long missionId
) {
validateAuthenticated(currentAuth);
MissionDetailResponse response = missionService.getMission(currentAuth.memberId(), missionId);
return ResponseEntity.ok(SuccessResponse.of(MissionSuccessCode.MISSION_DETAIL_SUCCESS, response));
}

@Override
@GetMapping("/hot")
public ResponseEntity<SuccessResponse<HotMissionListResponse>> getHotMissions() {
HotMissionListResponse response = hotMissionService.getCurrentHotMissions();
return ResponseEntity.ok(SuccessResponse.of(MissionSuccessCode.HOT_MISSION_LIST_SUCCESS, response));
}

private void validateAuthenticated(final CurrentAuth currentAuth) {
if (currentAuth == null) {
throw new JjinException(TokenErrorCode.INVALID_AUTHORIZATION_HEADER);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.JJIN.domain.mission.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.JJIN.domain.mission.controller.docs.MissionProofControllerDocs;
import com.JJIN.domain.mission.dto.request.MissionProofCommentCreateRequest;
import com.JJIN.domain.mission.dto.response.MissionProofCommentCreateResponse;
import com.JJIN.domain.mission.dto.response.MissionProofCommentListResponse;
import com.JJIN.domain.mission.dto.response.MissionProofFeedResponse;
import com.JJIN.domain.mission.dto.response.MissionProofLikeToggleResponse;
import com.JJIN.domain.mission.entity.enums.MissionProofFeedTab;
import com.JJIN.domain.mission.exception.MissionSuccessCode;
import com.JJIN.domain.mission.service.MissionProofService;
import com.JJIN.domain.mission.service.MissionProofCommentService;
import com.JJIN.global.auth.annotation.CurrentMember;
import com.JJIN.global.auth.dto.CurrentAuth;
import com.JJIN.global.auth.jwt.exception.TokenErrorCode;
import com.JJIN.global.exception.JjinException;
import com.JJIN.global.response.dto.SuccessResponse;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/missions/proofs")
@RequiredArgsConstructor
public class MissionProofController implements MissionProofControllerDocs {

private final MissionProofService missionProofService;
private final MissionProofCommentService missionProofCommentService;

@Override
@GetMapping("/feed")
public ResponseEntity<SuccessResponse<MissionProofFeedResponse>> getMissionProofFeed(
@CurrentMember CurrentAuth currentAuth,
@RequestParam(defaultValue = "LATEST") MissionProofFeedTab tab,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
if (currentAuth == null) {
throw new JjinException(TokenErrorCode.INVALID_AUTHORIZATION_HEADER);
}
MissionProofFeedResponse response =
missionProofService.getFeed(currentAuth.memberId(), tab, page, size);
return ResponseEntity.ok(
SuccessResponse.of(MissionSuccessCode.MISSION_PROOF_FEED_SUCCESS, response));
}

@Override
@PostMapping("/{proofId}/likes/toggle")
public ResponseEntity<SuccessResponse<MissionProofLikeToggleResponse>> toggleMissionProofLike(
@CurrentMember CurrentAuth currentAuth,
@PathVariable Long proofId
) {
if (currentAuth == null) {
throw new JjinException(TokenErrorCode.INVALID_AUTHORIZATION_HEADER);
}
MissionProofLikeToggleResponse response =
missionProofService.toggleLike(currentAuth.memberId(), proofId);
return ResponseEntity.ok(
SuccessResponse.of(MissionSuccessCode.MISSION_PROOF_LIKE_TOGGLE_SUCCESS, response));
}

@Override
@GetMapping("/{proofId}/comments")
public ResponseEntity<SuccessResponse<MissionProofCommentListResponse>> getMissionProofComments(
@CurrentMember CurrentAuth currentAuth,
@PathVariable Long proofId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
if (currentAuth == null) {
throw new JjinException(TokenErrorCode.INVALID_AUTHORIZATION_HEADER);
}
MissionProofCommentListResponse response =
missionProofCommentService.getComments(proofId, page, size);
return ResponseEntity.ok(
SuccessResponse.of(MissionSuccessCode.MISSION_PROOF_COMMENT_LIST_SUCCESS, response));
}

@Override
@PostMapping("/{proofId}/comments")
public ResponseEntity<SuccessResponse<MissionProofCommentCreateResponse>> createMissionProofComment(
@CurrentMember CurrentAuth currentAuth,
@PathVariable Long proofId,
@RequestBody(required = false) MissionProofCommentCreateRequest request
) {
if (currentAuth == null) {
throw new JjinException(TokenErrorCode.INVALID_AUTHORIZATION_HEADER);
}
MissionProofCommentCreateResponse response = missionProofCommentService.createComment(
currentAuth.memberId(),
proofId,
request
);
return ResponseEntity.ok(
SuccessResponse.of(MissionSuccessCode.MISSION_PROOF_COMMENT_CREATE_SUCCESS, response));
}
}
Loading
Loading