비공개 메모 기능 추가 + 아이콘 개선

- 식당별 1:1 비공개 메모 CRUD (user_memos 테이블)
- 내 기록에 리뷰/메모 탭 분리
- 백오피스 유저 관리에 메모 수/상세 표시
- 리뷰/메모 작성 시 현재 날짜 기본값
- 지도우선/목록우선 버튼 Material Symbols 아이콘 적용

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-12 14:10:06 +09:00
parent 88c1b4243e
commit 3134994817
15 changed files with 667 additions and 45 deletions

View File

@@ -1,7 +1,9 @@
package com.tasteby.controller;
import com.tasteby.domain.Memo;
import com.tasteby.domain.Restaurant;
import com.tasteby.domain.Review;
import com.tasteby.service.MemoService;
import com.tasteby.service.ReviewService;
import com.tasteby.service.UserService;
import org.springframework.web.bind.annotation.*;
@@ -15,10 +17,12 @@ public class AdminUserController {
private final UserService userService;
private final ReviewService reviewService;
private final MemoService memoService;
public AdminUserController(UserService userService, ReviewService reviewService) {
public AdminUserController(UserService userService, ReviewService reviewService, MemoService memoService) {
this.userService = userService;
this.reviewService = reviewService;
this.memoService = memoService;
}
@GetMapping
@@ -39,4 +43,9 @@ public class AdminUserController {
public List<Review> userReviews(@PathVariable String userId) {
return reviewService.findByUser(userId, 100, 0);
}
@GetMapping("/{userId}/memos")
public List<Memo> userMemos(@PathVariable String userId) {
return memoService.findByUser(userId);
}
}

View File

@@ -0,0 +1,59 @@
package com.tasteby.controller;
import com.tasteby.domain.Memo;
import com.tasteby.security.AuthUtil;
import com.tasteby.service.MemoService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class MemoController {
private final MemoService memoService;
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
@GetMapping("/restaurants/{restaurantId}/memo")
public Memo getMemo(@PathVariable String restaurantId) {
String userId = AuthUtil.getUserId();
Memo memo = memoService.findByUserAndRestaurant(userId, restaurantId);
if (memo == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No memo");
}
return memo;
}
@PostMapping("/restaurants/{restaurantId}/memo")
public Memo upsertMemo(@PathVariable String restaurantId,
@RequestBody Map<String, Object> body) {
String userId = AuthUtil.getUserId();
Double rating = body.get("rating") != null
? ((Number) body.get("rating")).doubleValue() : null;
String text = (String) body.get("memo_text");
LocalDate visitedAt = body.get("visited_at") != null
? LocalDate.parse((String) body.get("visited_at")) : null;
return memoService.upsert(userId, restaurantId, rating, text, visitedAt);
}
@GetMapping("/users/me/memos")
public List<Memo> myMemos() {
return memoService.findByUser(AuthUtil.getUserId());
}
@DeleteMapping("/restaurants/{restaurantId}/memo")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteMemo(@PathVariable String restaurantId) {
String userId = AuthUtil.getUserId();
if (!memoService.delete(userId, restaurantId)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No memo");
}
}
}

View File

@@ -0,0 +1,22 @@
package com.tasteby.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Memo {
private String id;
private String userId;
private String restaurantId;
private Double rating;
private String memoText;
private String visitedAt;
private String createdAt;
private String updatedAt;
private String restaurantName;
}

View File

@@ -22,4 +22,5 @@ public class UserInfo {
private String createdAt;
private int favoriteCount;
private int reviewCount;
private int memoCount;
}

View File

@@ -0,0 +1,32 @@
package com.tasteby.mapper;
import com.tasteby.domain.Memo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MemoMapper {
Memo findByUserAndRestaurant(@Param("userId") String userId,
@Param("restaurantId") String restaurantId);
void insertMemo(@Param("id") String id,
@Param("userId") String userId,
@Param("restaurantId") String restaurantId,
@Param("rating") Double rating,
@Param("memoText") String memoText,
@Param("visitedAt") String visitedAt);
int updateMemo(@Param("userId") String userId,
@Param("restaurantId") String restaurantId,
@Param("rating") Double rating,
@Param("memoText") String memoText,
@Param("visitedAt") String visitedAt);
int deleteMemo(@Param("userId") String userId,
@Param("restaurantId") String restaurantId);
List<Memo> findByUser(@Param("userId") String userId);
}

View File

@@ -0,0 +1,44 @@
package com.tasteby.service;
import com.tasteby.domain.Memo;
import com.tasteby.mapper.MemoMapper;
import com.tasteby.util.IdGenerator;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
@Service
public class MemoService {
private final MemoMapper mapper;
public MemoService(MemoMapper mapper) {
this.mapper = mapper;
}
public Memo findByUserAndRestaurant(String userId, String restaurantId) {
return mapper.findByUserAndRestaurant(userId, restaurantId);
}
@Transactional
public Memo upsert(String userId, String restaurantId, Double rating, String memoText, LocalDate visitedAt) {
String visitedStr = visitedAt != null ? visitedAt.toString() : null;
Memo existing = mapper.findByUserAndRestaurant(userId, restaurantId);
if (existing != null) {
mapper.updateMemo(userId, restaurantId, rating, memoText, visitedStr);
} else {
mapper.insertMemo(IdGenerator.newId(), userId, restaurantId, rating, memoText, visitedStr);
}
return mapper.findByUserAndRestaurant(userId, restaurantId);
}
public boolean delete(String userId, String restaurantId) {
return mapper.deleteMemo(userId, restaurantId) > 0;
}
public List<Memo> findByUser(String userId) {
return mapper.findByUser(userId);
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tasteby.mapper.MemoMapper">
<resultMap id="memoResultMap" type="com.tasteby.domain.Memo">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="restaurantId" column="restaurant_id"/>
<result property="rating" column="rating"/>
<result property="memoText" column="memo_text" typeHandler="com.tasteby.config.ClobTypeHandler"/>
<result property="visitedAt" column="visited_at"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
<result property="restaurantName" column="restaurant_name"/>
</resultMap>
<select id="findByUserAndRestaurant" resultMap="memoResultMap">
SELECT id, user_id, restaurant_id, rating, memo_text,
visited_at, created_at, updated_at
FROM user_memos
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</select>
<insert id="insertMemo">
INSERT INTO user_memos (id, user_id, restaurant_id, rating, memo_text, visited_at)
VALUES (#{id}, #{userId}, #{restaurantId}, #{rating}, #{memoText},
<choose>
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
<otherwise>NULL</otherwise>
</choose>)
</insert>
<update id="updateMemo">
UPDATE user_memos SET
rating = #{rating},
memo_text = #{memoText},
visited_at = <choose>
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
<otherwise>NULL</otherwise>
</choose>,
updated_at = SYSTIMESTAMP
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</update>
<delete id="deleteMemo">
DELETE FROM user_memos WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</delete>
<select id="findByUser" resultMap="memoResultMap">
SELECT m.id, m.user_id, m.restaurant_id, m.rating, m.memo_text,
m.visited_at, m.created_at, m.updated_at,
r.name AS restaurant_name
FROM user_memos m
LEFT JOIN restaurants r ON r.id = m.restaurant_id
WHERE m.user_id = #{userId}
ORDER BY m.updated_at DESC
</select>
</mapper>

View File

@@ -12,6 +12,7 @@
<result property="createdAt" column="created_at"/>
<result property="favoriteCount" column="favorite_count"/>
<result property="reviewCount" column="review_count"/>
<result property="memoCount" column="memo_count"/>
</resultMap>
<select id="findByProviderAndProviderId" resultMap="userResultMap">
@@ -38,10 +39,12 @@
<select id="findAllWithCounts" resultMap="userResultMap">
SELECT u.id, u.email, u.nickname, u.avatar_url, u.provider, u.created_at,
NVL(fav.cnt, 0) AS favorite_count,
NVL(rev.cnt, 0) AS review_count
NVL(rev.cnt, 0) AS review_count,
NVL(memo.cnt, 0) AS memo_count
FROM tasteby_users u
LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM user_favorites GROUP BY user_id) fav ON fav.user_id = u.id
LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM user_reviews GROUP BY user_id) rev ON rev.user_id = u.id
LEFT JOIN (SELECT user_id, COUNT(*) AS cnt FROM user_memos GROUP BY user_id) memo ON memo.user_id = u.id
ORDER BY u.created_at DESC
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
</select>