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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.sku.refit.domain.event.repository.EventRepository;
import com.sku.refit.domain.event.repository.EventReservationImageRepository;
import com.sku.refit.domain.event.repository.EventReservationRepository;
import com.sku.refit.domain.ticket.entity.TicketType;
import com.sku.refit.domain.ticket.service.TicketService;
import com.sku.refit.domain.user.entity.User;
import com.sku.refit.domain.user.service.UserService;
import com.sku.refit.global.exception.CustomException;
Expand All @@ -43,6 +45,7 @@ public class EventServiceImpl implements EventService {
private final S3Service s3Service;
private final UserService userService;
private final EventMapper eventMapper;
private final TicketService ticketService;

/* =========================
* Admin
Expand Down Expand Up @@ -308,6 +311,12 @@ public EventReservationResponse reserveEvent(
EventReservation reservation = eventMapper.toReservation(event, user, request);

eventReservationRepository.save(reservation);
ticketService.issueTicket(
TicketType.EVENT,
event.getId(),
user.getId(),
event.getDate() // 행사 필드에 종료 일자 추가시 종료 일자로 변경 필요
);

if (clothImageList != null && !clothImageList.isEmpty()) {
for (MultipartFile f : clothImageList) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.constant;

public enum TicketUseStatus {
UNUSED, // 사용전
USED, // 사용완료
EXPIRED // 사용만료
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.sku.refit.domain.mypage.dto.response.MyPageResponse.*;
import com.sku.refit.domain.post.dto.response.PostDetailResponse;
import com.sku.refit.global.page.response.InfiniteResponse;
import com.sku.refit.global.response.BaseResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "마이페이지", description = "마이페이지 관련 API")
@RequestMapping("/api/my")
public interface MyPageController {

@GetMapping("/tickets")
@Operation(
summary = "내 티켓 리스트 조회",
description =
"""
현재 로그인한 사용자의 티켓 목록을 페이징하여 조회합니다.

■ 반환 데이터
- 티켓 ID
- 티켓 타입 (EVENT / CLOTH)
- 티켓 상태 (UNUSED / USED / EXPIRED)
- 티켓명
- 위치 정보
- 설명
- QR payload URL
- 발급 시각
- 사용 시각 (사용 완료된 경우)
- 만료일

■ 정렬 기준
- 발급 시각(createdAt) 기준 내림차순 (최신 발급 티켓 우선)

■ 페이징
- page: 조회할 페이지 번호 (0부터 시작)
- size: 한 페이지에 포함될 티켓 개수
""")
ResponseEntity<BaseResponse<MyTicketsResponse>> getMyTickets(
@RequestParam int page, @RequestParam int size);

@GetMapping("/events/joined")
@Operation(
summary = "참여한 행사 조회",
description = "행사 예약시 발급되는 티켓 중 사용 완료된 티켓을 기준으로 최신순으로 참여한 행사 목록을 반환합니다.")
ResponseEntity<BaseResponse<JoinedEventsResponse>> getJoinedEvents();

@GetMapping("/posts")
@Operation(
summary = "내가 작성한 글 조회",
description =
"""
현재 로그인한 사용자가 작성한 게시글 목록을 커서 기반 무한스크롤로 조회합니다.

■ 커서 페이징 방식
- 첫 조회: lastPostId 생략
- 다음 조회: 직전 응답의 lastCursor 값을 lastPostId로 전달
- 정렬: id DESC (최신글 먼저)
- hasNext: 다음 페이지 존재 여부
- lastCursor: 다음 요청에 사용할 커서(마지막 항목의 postId)
""")
ResponseEntity<BaseResponse<InfiniteResponse<PostDetailResponse>>> getMyPosts(
@Parameter(description = "마지막으로 조회한 게시글 식별자(첫 조회 시 생략)", example = "10")
@RequestParam(required = false)
Long lastPostId,
@Parameter(description = "한 번에 조회할 게시글 개수", example = "10") @RequestParam(defaultValue = "10")
Integer size);

@GetMapping
@Operation(
summary = "마이페이지 홈 조회",
description =
"""
마이페이지 홈에 필요한 정보를 조회합니다.

로그인 여부, 사용자 정보, 교환 횟수, 총 누적 탄소 절감량과
탄소량 변경 이력[변경 시각, 변경 후 누적량, 변경량]을 반환합니다. (과거 → 최신순)

해당 이력 데이터는 그래프 시각화 용도로 사용됩니다.
""")
ResponseEntity<BaseResponse<MyHomeResponse>> getMyHome();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.controller;

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

import com.sku.refit.domain.mypage.dto.response.MyPageResponse.*;
import com.sku.refit.domain.mypage.service.MyPageService;
import com.sku.refit.domain.post.dto.response.PostDetailResponse;
import com.sku.refit.global.page.response.InfiniteResponse;
import com.sku.refit.global.response.BaseResponse;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class MyPageControllerImpl implements MyPageController {

private final MyPageService myPageService;

@Override
public ResponseEntity<BaseResponse<MyTicketsResponse>> getMyTickets(
@RequestParam int page, @RequestParam int size) {
return ResponseEntity.ok(BaseResponse.success(myPageService.getMyTickets(page, size)));
}

@Override
public ResponseEntity<BaseResponse<JoinedEventsResponse>> getJoinedEvents() {
return ResponseEntity.ok(BaseResponse.success(myPageService.getJoinedEvents()));
}

@Override
public ResponseEntity<BaseResponse<InfiniteResponse<PostDetailResponse>>> getMyPosts(
Long lastPostId, Integer size) {

return ResponseEntity.ok(BaseResponse.success(myPageService.getMyPosts(lastPostId, size)));
}

@Override
public ResponseEntity<BaseResponse<MyHomeResponse>> getMyHome() {
return ResponseEntity.ok(BaseResponse.success(myPageService.getMyHome()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.dto.response;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import com.sku.refit.domain.mypage.constant.TicketUseStatus;
import com.sku.refit.domain.ticket.entity.TicketType;
import com.sku.refit.domain.user.dto.response.UserDetailResponse;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;

public class MyPageResponse {

/* =========================
* Tickets (paged)
* ========================= */

@Getter
@Builder
public static class MyTicketsResponse {
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean hasNext;
private List<MyTicketItem> items;
}

@Getter
@Builder
@Schema(title = "MyTicketItem DTO", description = "마이페이지 티켓 리스트 아이템")
public static class MyTicketItem {

@Schema(description = "티켓 ID", example = "10")
private Long ticketId;

@Schema(description = "티켓 타입", example = "EVENT")
private TicketType type;

@Schema(description = "사용 상태(사용전/사용완료/사용만료)", example = "UNUSED")
private TicketUseStatus status;

@Schema(description = "티켓명(표시용)", example = "겨울 의류 나눔 행사")
private String ticketName;

@Schema(description = "위치(표시용)", example = "서울 성동구")
private String location;

@Schema(description = "설명(표시용)", example = "따뜻한 겨울을 위한 의류 기부 행사입니다.")
private String description;

@Schema(
description = "QR payload(URL)",
example = "https://api/refitlab.site/ticket?v=1&token=xxx")
private String url;

@Schema(description = "발급 시각", example = "2025-12-01T10:00:00")
private LocalDateTime issuedAt;

@Schema(description = "사용 시각", example = "2025-12-24T12:30:00")
private LocalDateTime usedAt;

@Schema(description = "유효기간", example = "2025-12-24T23:59:59")
private LocalDate expiresAt;
}

/* =========================
* Joined Events
* ========================= */

@Getter
@Builder
public static class JoinedEventsResponse {
private List<JoinedEventItem> items;
}

@Getter
@Builder
@Schema(title = "JoinedEventItem DTO", description = "참가한 행사 응답")
public static class JoinedEventItem {

@Schema(description = "행사 식별자", example = "1")
private Long eventId;

@Schema(description = "썸네일 이미지 URL")
private String thumbnailUrl;

@Schema(description = "행사명", example = "겨울 의류 나눔 행사")
private String name;

@Schema(description = "행사 설명", example = "따뜻한 겨울을 위한 의류 기부 행사입니다.")
private String description;

@Schema(description = "행사 날짜", example = "2025-12-24")
private LocalDate date;

@Schema(description = "행사 장소", example = "서울 성동구")
private String location;
}

/* =========================
* Home
* ========================= */

@Getter
@Builder
@Schema(title = "MyPageHomeResponse DTO", description = "마이페이지 홈(/api/my) 응답")
public static class MyHomeResponse {

@Schema(description = "로그인 여부", example = "true")
private Boolean isLoggedIn;

@Schema(description = "사용자 정보 (비로그인 시 null)")
private UserDetailResponse user;

@Schema(description = "나의 교환 횟수", example = "5")
private Integer exchangeCount;

@Schema(description = "총 줄인 탄소량(g)", example = "750")
private Long totalReducedCarbonG;

@Schema(description = "탄소량 변경 이력(최신순)")
private List<CarbonChangeItem> carbonChangeList;
}

@Getter
@Builder
@Schema(title = "CarbonChangeItem DTO", description = "탄소량 변경 이력 아이템")
public static class CarbonChangeItem {

@Schema(description = "변경 일시", example = "2025-12-24T12:30:00")
private LocalDateTime changedAt;

@Schema(description = "변경일까지의 누적값", example = "40")
private Long totalAfterG;

@Schema(description = "변경량(g). 교환이면 +20", example = "20")
private Long deltaG;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.entity;

import java.time.LocalDateTime;

import jakarta.persistence.*;

import com.sku.refit.domain.user.entity.User;
import com.sku.refit.global.common.BaseTimeEntity;

import lombok.*;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(
name = "carbon_reduction_history",
indexes = {
@Index(name = "idx_carbon_hist_user", columnList = "user_id"),
@Index(name = "idx_carbon_hist_changed_at", columnList = "changed_at")
})
public class CarbonReductionHistory extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Column(name = "changed_at", nullable = false)
private LocalDateTime changedAt;

/** 변경량(g). 교환이면 +20 */
@Column(name = "delta_g", nullable = false)
private Long deltaG;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) SKU 다시입을Lab
*/
package com.sku.refit.domain.mypage.exception;

import org.springframework.http.HttpStatus;

import com.sku.refit.global.exception.model.BaseErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum MyPageErrorCode implements BaseErrorCode {
TICKETS_FETCH_FAILED("MYPAGE001", "티켓 목록 조회에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
JOINED_EVENTS_FETCH_FAILED("MYPAGE002", "참여한 행사 조회에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
MY_POSTS_FETCH_FAILED("MYPAGE003", "내가 작성한 글 조회에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
CARBON_ADD_FAILED("MYPAGE004", "탄소량 반영에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
MY_HOME_FETCH_FAILED("MYPAGE005", "내 홈 조회에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
;
private final String code;
private final String message;
private final HttpStatus status;
}
Loading