#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
132 lines
5.6 KiB
XML
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>
|