Migrate to MyBatis with proper Controller→Service→Mapper layering

- Add MyBatis Spring Boot Starter with XML mappers and domain classes
- Create 9 mapper interfaces + XML: Restaurant, Video, Channel, Review,
  User, Stats, DaemonConfig, Search, Vector
- Create 10 domain classes with Lombok: Restaurant, VideoSummary,
  VideoDetail, VideoRestaurantLink, Channel, Review, UserInfo,
  DaemonConfig, SiteVisitStats, VectorSearchResult
- Create 7 new service classes: RestaurantService, VideoService,
  ChannelService, ReviewService, UserService, StatsService,
  DaemonConfigService
- Refactor all controllers to be thin (HTTP + auth only), delegating
  business logic to services
- Refactor SearchService, PipelineService, DaemonScheduler, AuthService,
  YouTubeService to use mappers/services instead of JDBC/repositories
- Add Jackson SNAKE_CASE property naming for consistent API responses
- Add ClobTypeHandler for Oracle CLOB→String in MyBatis
- Add IdGenerator utility for centralized UUID generation
- Delete old repository/ package (6 files), JdbcConfig, LowerCaseKeyAdvice
- VectorService retains JDBC for Oracle VECTOR type support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-09 21:13:44 +09:00
parent 91d0ad4598
commit c16add08c3
63 changed files with 2155 additions and 1483 deletions

View File

@@ -0,0 +1,130 @@
<?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">
SELECT ROUND(AVG(rating), 1) 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>