Files
tasteby/backend-java/src/main/resources/mybatis/mapper/ReviewMapper.xml
joungmin 4b02293046 fix(crud): P4-1 백엔드 CRUD 결함 일괄 수정 (#290+#294+#295)
#294 (리뷰/메모):
- MemoService.upsert: 동시성 INSERT 시 DuplicateKeyException 폴백 → UPDATE
- ReviewService.toggleFavorite: 동시성 INSERT 시 DuplicateKeyException ignored (토글 ON)
- ReviewController: rating(0~5) Bean validation 헬퍼, body.rating null/비숫자 → 400
- ReviewMapper.xml getAvgRating: NVL로 0건 시에도 0.0 보장

#295 (채널):
- ChannelController.create: typed DataIntegrityViolationException으로 유니크 충돌 감지 (제약명 문자열 매칭 폐기)
- ChannelController.create: channel_id/channel_name null/빈값 → 400
- ChannelService.deactivate: "UC..." 형식 검증으로 명시적 분기 (이전 폴백 방식의 의도 모호함 해결)
- ChannelMapper.xml findByChannelId: description/tags/sort_order까지 SELECT

#290 (식당 CRUD):
- RestaurantController: @PreDestroy로 virtual thread executor shutdown
- RestaurantController: 캐시 역직렬화 실패를 silent ignore → log.warn + cache.del 자동 evict
- RestaurantController: setTablingUrl/setCatchtableUrl URL 스킴 화이트리스트 검증
- CacheService: 단일 키 del() 메서드 추가

후속 분리:
- #333 (#290 DTO 화이트리스트 + DDG 대체)
- #334 (#295 cache.flush 세분화 + scan 비동기)
- #335 (#294 테스트)

Refs: #290 #294 #295
2026-06-15 14:14:41 +09:00

132 lines
5.6 KiB
XML

<?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.ReviewMapper">
<resultMap id="reviewResultMap" type="com.tasteby.domain.Review">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="restaurantId" column="restaurant_id"/>
<result property="rating" column="rating"/>
<result property="reviewText" column="review_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="userNickname" column="nickname"/>
<result property="userAvatarUrl" column="avatar_url"/>
<result property="restaurantName" column="restaurant_name"/>
</resultMap>
<resultMap id="restaurantResultMap" type="com.tasteby.domain.Restaurant">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="address" column="address"/>
<result property="region" column="region"/>
<result property="latitude" column="latitude"/>
<result property="longitude" column="longitude"/>
<result property="cuisineType" column="cuisine_type"/>
<result property="priceRange" column="price_range"/>
<result property="googlePlaceId" column="google_place_id"/>
<result property="businessStatus" column="business_status"/>
<result property="rating" column="rating"/>
<result property="ratingCount" column="rating_count"/>
<result property="updatedAt" column="created_at"/>
</resultMap>
<insert id="insertReview">
INSERT INTO user_reviews (id, user_id, restaurant_id, rating, review_text, visited_at)
VALUES (#{id}, #{userId}, #{restaurantId}, #{rating}, #{reviewText},
<choose>
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
<otherwise>NULL</otherwise>
</choose>)
</insert>
<update id="updateReview">
UPDATE user_reviews SET
rating = COALESCE(#{rating}, rating),
review_text = COALESCE(#{reviewText}, review_text),
visited_at = COALESCE(
<choose>
<when test="visitedAt != null">TO_DATE(#{visitedAt}, 'YYYY-MM-DD')</when>
<otherwise>NULL</otherwise>
</choose>, visited_at),
updated_at = SYSTIMESTAMP
WHERE id = #{id} AND user_id = #{userId}
</update>
<delete id="deleteReview">
DELETE FROM user_reviews WHERE id = #{id} AND user_id = #{userId}
</delete>
<select id="findById" resultMap="reviewResultMap">
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
WHERE r.id = #{id}
</select>
<select id="findByRestaurant" resultMap="reviewResultMap">
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
WHERE r.restaurant_id = #{restaurantId}
ORDER BY r.created_at DESC
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
</select>
<select id="getAvgRating" resultType="map">
<!-- #294 — review 0건이면 AVG는 NULL → 클라이언트 NaN 처리 부담. NVL로 0.0 보장. -->
SELECT NVL(ROUND(AVG(rating), 1), 0) AS avg_rating, COUNT(*) AS review_count
FROM user_reviews
WHERE restaurant_id = #{restaurantId}
</select>
<select id="findByUser" resultMap="reviewResultMap">
SELECT r.id, r.user_id, r.restaurant_id, r.rating, r.review_text,
r.visited_at, r.created_at, r.updated_at,
u.nickname, u.avatar_url,
rest.name AS restaurant_name
FROM user_reviews r
JOIN tasteby_users u ON u.id = r.user_id
LEFT JOIN restaurants rest ON rest.id = r.restaurant_id
WHERE r.user_id = #{userId}
ORDER BY r.created_at DESC
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
</select>
<select id="countFavorite" resultType="int">
SELECT COUNT(*) FROM user_favorites
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</select>
<insert id="insertFavorite">
INSERT INTO user_favorites (id, user_id, restaurant_id)
VALUES (#{id}, #{userId}, #{restaurantId})
</insert>
<delete id="deleteFavorite">
DELETE FROM user_favorites
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</delete>
<select id="findFavoriteId" resultType="string">
SELECT id FROM user_favorites
WHERE user_id = #{userId} AND restaurant_id = #{restaurantId}
</select>
<select id="getUserFavorites" resultMap="restaurantResultMap">
SELECT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
r.cuisine_type, r.price_range, r.google_place_id,
r.business_status, r.rating, r.rating_count, f.created_at
FROM user_favorites f
JOIN restaurants r ON r.id = f.restaurant_id
WHERE f.user_id = #{userId}
ORDER BY f.created_at DESC
</select>
</mapper>