Merge branch 'dev'
Deploy / deploy (push) Failing after 15m14s

This commit is contained in:
ByungCheol
2026-06-28 14:44:25 +09:00
7 changed files with 119 additions and 1 deletions
@@ -0,0 +1,75 @@
package com.sb.web.account.service;
import com.sb.web.account.domain.DefaultCategory;
import com.sb.web.account.mapper.DefaultCategoryMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 기본(디폴트) 분류 초기 시드. default_category 가 비어 있을 때만 한 번 생성한다.
* (관리자가 이후 자유롭게 수정/삭제 가능 — 비어 있을 때만 채우므로 운영 데이터를 덮어쓰지 않음)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultCategorySeeder implements ApplicationRunner {
private final DefaultCategoryMapper mapper;
// 대분류 → 소분류 목록 (입력 순서 유지)
private static final Map<String, List<String>> EXPENSE = new LinkedHashMap<>();
private static final Map<String, List<String>> INCOME = new LinkedHashMap<>();
static {
EXPENSE.put("식비", List.of("외식", "식료품", "카페/간식", "배달"));
EXPENSE.put("교통", List.of("대중교통", "택시", "주유", "주차/통행료"));
EXPENSE.put("주거/통신", List.of("월세/관리비", "공과금", "통신비", "인터넷"));
EXPENSE.put("생활", List.of("생필품", "의류/미용", "가구/가전"));
EXPENSE.put("건강/의료", List.of("병원", "약국", "운동"));
EXPENSE.put("문화/여가", List.of("영화/공연", "여행", "취미", "구독서비스"));
EXPENSE.put("교육", List.of("학원", "도서", "강의"));
EXPENSE.put("경조사", List.of("축의금", "부의금", "선물"));
EXPENSE.put("금융", List.of("보험", "대출이자", "수수료"));
EXPENSE.put("기타", List.of());
INCOME.put("급여", List.of("월급", "상여금"));
INCOME.put("부수입", List.of("용돈", "이자/배당", "환급"));
INCOME.put("기타수입", List.of("중고판매", "기타"));
}
@Override
@Transactional
public void run(ApplicationArguments args) {
if (!mapper.findAll().isEmpty()) {
return; // 이미 데이터가 있으면 시드하지 않음
}
int n = seed("EXPENSE", EXPENSE) + seed("INCOME", INCOME);
log.info("[default-category] 기본 분류 시드 생성: {}건", n);
}
private int seed(String type, Map<String, List<String>> tree) {
int count = 0;
int majorOrder = 0;
for (Map.Entry<String, List<String>> e : tree.entrySet()) {
DefaultCategory major = DefaultCategory.builder()
.type(type).name(e.getKey()).parentId(null).sortOrder(majorOrder++).build();
mapper.insert(major); // 생성키(id) 채워짐
count++;
int subOrder = 0;
for (String child : e.getValue()) {
mapper.insert(DefaultCategory.builder()
.type(type).name(child).parentId(major.getId()).sortOrder(subOrder++).build());
count++;
}
}
return count;
}
}
@@ -87,6 +87,13 @@ public class AuthController {
return java.util.Map.of("points", pointService.getPoints(current.getId())); return java.util.Map.of("points", pointService.getPoints(current.getId()));
} }
/** 포인트 적립/차감 내역 (최신순) */
@GetMapping("/point-history")
public java.util.List<com.sb.web.point.PointHistoryResponse> pointHistory(
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return pointService.getHistory(current.getId());
}
@PutMapping("/password") @PutMapping("/password")
public ResponseEntity<Void> changePassword( public ResponseEntity<Void> changePassword(
@Valid @RequestBody PasswordChangeRequest req, @Valid @RequestBody PasswordChangeRequest req,
@@ -25,7 +25,8 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// 인증: 로그인 필요한 경로 (관리자 경로 포함 — SessionUser 세팅용으로 먼저 실행) // 인증: 로그인 필요한 경로 (관리자 경로 포함 — SessionUser 세팅용으로 먼저 실행)
registry.addInterceptor(authInterceptor) registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/auth/me", "/api/auth/points", "/api/auth/logout", "/api/auth/password", .addPathPatterns("/api/auth/me", "/api/auth/points", "/api/auth/point-history",
"/api/auth/logout", "/api/auth/password",
"/api/auth/verify-password", "/api/auth/profile", "/api/auth/profile-image", "/api/auth/verify-password", "/api/auth/profile", "/api/auth/profile-image",
"/api/board/**", "/api/admin/**", "/api/account/**"); "/api/board/**", "/api/admin/**", "/api/account/**");
// 인가: 관리자 전용 경로 (authInterceptor 다음에 실행되어 role 검사) // 인가: 관리자 전용 경로 (authInterceptor 다음에 실행되어 role 검사)
@@ -0,0 +1,16 @@
package com.sb.web.point;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 포인트 적립/차감 내역 항목.
*/
@Data
public class PointHistoryResponse {
private Integer amount; // 증감 포인트(+/-)
private String reason; // BOARD_WRITE 등
private LocalDateTime createdAt;
}
@@ -38,4 +38,9 @@ public class PointService {
Long p = pointMapper.getPoints(memberId); Long p = pointMapper.getPoints(memberId);
return p == null ? 0L : p; return p == null ? 0L : p;
} }
/** 최근 포인트 적립/차감 내역 (최신순) */
public java.util.List<PointHistoryResponse> getHistory(Long memberId) {
return pointMapper.findHistory(memberId, 100);
}
} }
@@ -1,8 +1,11 @@
package com.sb.web.point.mapper; package com.sb.web.point.mapper;
import com.sb.web.point.PointHistoryResponse;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List;
/** /**
* 포인트(member.points) 및 적립 이력(point_history) 매퍼. * 포인트(member.points) 및 적립 이력(point_history) 매퍼.
*/ */
@@ -17,4 +20,7 @@ public interface PointMapper {
int addPoints(@Param("memberId") Long memberId, @Param("amount") int amount); int addPoints(@Param("memberId") Long memberId, @Param("amount") int amount);
Long getPoints(@Param("memberId") Long memberId); Long getPoints(@Param("memberId") Long memberId);
/** 최근 적립/차감 내역 (최신순, 최대 limit건) */
List<PointHistoryResponse> findHistory(@Param("memberId") Long memberId, @Param("limit") int limit);
} }
@@ -21,4 +21,12 @@
SELECT points FROM member WHERE id = #{memberId} SELECT points FROM member WHERE id = #{memberId}
</select> </select>
<select id="findHistory" resultType="com.sb.web.point.PointHistoryResponse">
SELECT amount, reason, created_at
FROM point_history
WHERE member_id = #{memberId}
ORDER BY id DESC
LIMIT #{limit}
</select>
</mapper> </mapper>