Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ jobs:
scripts/verify-production-migration-005.sh
scripts/verify-production-migration-006.sh

- name: Verify production migrations 007-013 schema
- name: Verify production migrations 007-015 schema
working-directory: ${{ github.workspace }}
run: |
chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-007-013.sh
scripts/verify-production-migration-007-013.sh
chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-007-015.sh
scripts/verify-production-migration-007-015.sh
- run: ./gradlew spotlessCheck
- run: ./gradlew test build -x spotlessCheck

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ jobs:
scripts/verify-production-migration-005.sh
scripts/verify-production-migration-006.sh

- name: Verify production migrations 007-013 schema
- name: Verify production migrations 007-015 schema
working-directory: ${{ github.workspace }}
run: |
chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-007-013.sh
scripts/verify-production-migration-007-013.sh
chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-007-015.sh
scripts/verify-production-migration-007-015.sh

- name: Build and Test with Gradle
run: ./gradlew test build -x spotlessCheck
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ frontend/.next/
frontend/node_modules/
frontend/out/
frontend/*.log
frontend/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

import com.ikae.snowthing.domain.member.validation.NicknamePolicy;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -15,7 +17,7 @@
public class MemberProfileUpdateRequest {

@NotBlank(message = "닉네임은 필수 입력값입니다.")
@Pattern(regexp = "^[a-zA-Z0-9가-힣]{2,10}$", message = "닉네임은 2자 이상 10자 이하의 한글, 영문, 숫자이어야 합니다.")
@Pattern(regexp = NicknamePolicy.REGEXP, message = NicknamePolicy.MESSAGE)
private String nickname;

private String bio;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.constraints.Size;

import com.ikae.snowthing.domain.member.entity.Member;
import com.ikae.snowthing.domain.member.validation.NicknamePolicy;

import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -35,7 +36,7 @@ public class MemberSignUpRequest {
private String emailVerificationToken;

@NotBlank(message = "닉네임은 필수 입력값입니다.")
@jakarta.validation.constraints.Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하이어야 합니다.")
@Pattern(regexp = NicknamePolicy.REGEXP, message = NicknamePolicy.MESSAGE)
private String nickname;

private String bio;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import java.util.Optional;

import jakarta.persistence.LockModeType;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.ikae.snowthing.domain.member.entity.Member;

Expand All @@ -15,4 +20,8 @@ public interface MemberRepository extends JpaRepository<Member, Long>, MemberRep
Optional<Member> findByPublicId(String publicId);

Optional<Member> findByEmail(String email);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT m FROM Member m WHERE m.id = :memberId")
Optional<Member> findByIdForUpdate(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public class MasterDataService {
private final RidingStyleRepository ridingStyleRepository;

public List<ResortResponse> getAllResorts() {
return resortRepository.findAll().stream().map(ResortResponse::from).toList();
return resortRepository.findAllByActiveTrueOrderByDisplayOrderAscIdAsc().stream()
.map(ResortResponse::from)
.toList();
}

public List<RidingStyleResponse> getAllRidingStyles() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ikae.snowthing.domain.member.validation;

public final class NicknamePolicy {

public static final int MIN_LENGTH = 2;
public static final int MAX_LENGTH = 10;
public static final String REGEXP = "^[a-zA-Z0-9가-힣]{2,10}$";
public static final String MESSAGE = "닉네임은 2자 이상 10자 이하의 한글, 영문, 숫자이어야 합니다.";

private NicknamePolicy() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class PostService {

private static final String ANONYMOUS_CATEGORY_CODE = "ANONYMOUS";
private static final String MARKET_CATEGORY_CODE = "MARKET";
private static final String CREW_CATEGORY_CODE = "CREW";
private static final String SEASON_ROOM_CATEGORY_CODE = "SEASON_ROOM";
private static final String DEFAULT_WRITER_IP = "127.0.0.1";
private static final int FIRST_IMAGE_SORT_ORDER = 1;
private static final int MAX_OFFSET_PAGE = 100;
Expand All @@ -59,6 +61,15 @@ public PostResponse createPost(
() -> new CustomAuthException(ErrorCode.POST_CATEGORY_NOT_FOUND));
rejectMarketCategory(category);

if (isAnonymousDisallowedCategory(category.getCode())) {
if (userDetails == null) {
throw new CustomAuthException(ErrorCode.INVALID_CREDENTIALS);
}
if (request.isAnonymous()) {
throw new CustomAuthException(ErrorCode.ANONYMOUS_POST_NOT_ALLOWED);
}
}

Member member = null;
String encodedPassword = null;

Expand Down Expand Up @@ -208,6 +219,10 @@ public PostResponse updatePost(
rejectMarketPost(post);
rejectMarketCategory(category);

if (isAnonymousDisallowedCategory(category.getCode()) && post.isAnonymous()) {
throw new CustomAuthException(ErrorCode.ANONYMOUS_POST_NOT_ALLOWED);
}

post.update(request.title(), request.content(), category);
post.replaceImages(toPostImages(request.imageUrls()));
return PostResponse.from(post);
Expand Down Expand Up @@ -304,6 +319,11 @@ private void rejectMarketCategory(PostCategory category) {
}
}

private boolean isAnonymousDisallowedCategory(String categoryCode) {
return CREW_CATEGORY_CODE.equalsIgnoreCase(categoryCode)
|| SEASON_ROOM_CATEGORY_CODE.equalsIgnoreCase(categoryCode);
}

private boolean isAdmin(CustomUserDetails userDetails) {
return userDetails != null
&& userDetails.getAuthorities().stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ikae.snowthing.domain.resortreport.controller;

import jakarta.validation.Valid;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.ikae.snowthing.domain.resortreport.dto.ResortReportModerationStatusUpdateRequest;
import com.ikae.snowthing.domain.resortreport.service.ResortReportService;
import com.ikae.snowthing.global.security.CustomUserDetails;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/v1/admin/resort-reports")
@RequiredArgsConstructor
public class ResortReportAdminController {

private final ResortReportService resortReportService;

@PatchMapping("/{reportId}/moderation-status")
public ResponseEntity<Void> updateModerationStatus(
@PathVariable Long reportId,
@Valid @RequestBody ResortReportModerationStatusUpdateRequest request,
@AuthenticationPrincipal CustomUserDetails userDetails) {
resortReportService.updateModerationStatus(
reportId, request.moderationStatus(), userDetails);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.ikae.snowthing.domain.resortreport.controller;

import jakarta.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import com.ikae.snowthing.domain.resortreport.dto.ResortReportCreateRequest;
import com.ikae.snowthing.domain.resortreport.dto.ResortReportResponse;
import com.ikae.snowthing.domain.resortreport.service.ResortReportService;
import com.ikae.snowthing.global.common.dto.CursorPageResponse;
import com.ikae.snowthing.global.error.ErrorCode;
import com.ikae.snowthing.global.exception.CustomException;
import com.ikae.snowthing.global.security.CustomUserDetails;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/v1/resort-reports")
@RequiredArgsConstructor
public class ResortReportController {

private final ResortReportService resortReportService;

@PostMapping
public ResponseEntity<ResortReportResponse> createReport(
@Valid @RequestBody ResortReportCreateRequest request,
@AuthenticationPrincipal CustomUserDetails userDetails) {
if (userDetails == null || userDetails.getMember() == null) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
ResortReportResponse response =
resortReportService.createReport(userDetails.getMember().getId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@GetMapping("/today")
public ResponseEntity<CursorPageResponse<ResortReportResponse>> getTodayReports(
@RequestParam(required = false) Long resortId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@AuthenticationPrincipal CustomUserDetails userDetails) {
CursorPageResponse<ResortReportResponse> responses =
resortReportService.getTodayReports(resortId, page, size, userDetails);
return ResponseEntity.ok(responses);
}

@DeleteMapping("/{reportId}")
public ResponseEntity<Void> deleteReport(
@PathVariable Long reportId, @AuthenticationPrincipal CustomUserDetails userDetails) {
resortReportService.deleteReport(reportId, userDetails);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ikae.snowthing.domain.resortreport.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ResortReportCreateRequest {

@NotNull(message = "리조트를 선택해 주세요.")
private Long resortId;

@NotBlank(message = "설질 제보 내용을 입력해 주세요.")
@Size(max = 100, message = "설질 제보 내용은 최대 100자까지 작성할 수 있습니다.")
private String content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ikae.snowthing.domain.resortreport.dto;

import jakarta.validation.constraints.NotNull;

import com.ikae.snowthing.domain.resortreport.entity.ResortReportStatus;

public record ResortReportModerationStatusUpdateRequest(
@NotNull ResortReportStatus moderationStatus) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.ikae.snowthing.domain.resortreport.dto;

import java.time.OffsetDateTime;
import java.time.ZoneId;

import com.ikae.snowthing.domain.resortreport.entity.ResortReport;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ResortReportResponse {

private final Long reportId;
private final Long resortId;
private final String resortName;
private final String resortCode;
private final String authorNickname;
private final String content;
private final OffsetDateTime createdAt;
private final boolean canDelete;

public static ResortReportResponse from(ResortReport report, boolean canDelete) {
return ResortReportResponse.builder()
.reportId(report.getId())
.resortId(report.getResort().getId())
.resortName(report.getResort().getName())
.resortCode(report.getResort().getCode())
.authorNickname(report.getAuthor().getNickname())
.content(report.getContent())
.createdAt(report.getCreatedAt().atZone(ZoneId.of("Asia/Seoul")).toOffsetDateTime())
.canDelete(canDelete)
.build();
}
}
Loading
Loading