1414import com .yogieat .region .domain .RegionMaster ;
1515import com .yogieat .region .domain .RegionSummary ;
1616import com .yogieat .region .service .RegionService ;
17+ import com .yogieat .restaurant .domain .Restaurant ;
1718import com .yogieat .restaurant .domain .SuggestionRestaurant ;
1819import java .util .ArrayList ;
1920import java .util .Arrays ;
21+ import java .util .HashMap ;
2022import java .util .List ;
2123import java .util .Map ;
2224import java .util .Optional ;
@@ -62,8 +64,6 @@ public class RestaurantCollectionProcessor {
6264 private static final int RESTAURANTS_PER_REQUEST = 10 ;
6365 private static final int KAKAO_COLLECTION_CONCURRENT_PERMITS = 3 ;
6466 private static final int MIN_REVIEW_COUNT = 30 ;
65- private static final List <String > HIGH_VOLUME_REGION_CODES = List .of ("GANGNAM" , "HONGDAE" );
66- private static final int DEFAULT_REGION_LIMIT = 50 ;
6767
6868 private final Semaphore kakaoApiSemaphore = new Semaphore (KAKAO_COLLECTION_CONCURRENT_PERMITS );
6969
@@ -74,29 +74,32 @@ public void collectAllRegions() {
7474 return ;
7575 }
7676
77- ConcurrentHashMap < Long , Long > regionCounts = new ConcurrentHashMap <>( loadRegionCounts ( activeRegions ));
78- List < RegionSummary > regionsToCollect = filterRegionsNeedingCollection (activeRegions , regionCounts );
77+ List < RestaurantCollectionPlan . Request > collectionRequests =
78+ RestaurantCollectionPlan . create (activeRegions , FOOD_CATEGORIES . size () );
7979
80- if (regionsToCollect .isEmpty ()) {
80+ if (collectionRequests .isEmpty ()) {
81+ log .info ("All active regions reached restaurant collection limit: {} restaurants" ,
82+ RestaurantCollectionPlan .REGION_RESTAURANT_LIMIT );
8183 return ;
8284 }
8385
84- List <String > locationsToCollect = regionsToCollect .stream ()
85- .map (summary -> summary .region ().displayName ())
86- .toList ();
87- Map <String , RegionSummary > regionByLocation = regionsToCollect .stream ()
86+ logCollectionPlan (collectionRequests );
87+
88+ Map <String , RestaurantCollectionPlan .Request > requestByLocation = collectionRequests .stream ()
8889 .collect (Collectors .toMap (
89- summary -> summary .region ().displayName (),
90+ request -> request . regionSummary () .region ().displayName (),
9091 Function .identity (),
91- keepExistingRegion ()
92+ keepExistingRequest ()
9293 ));
94+ ConcurrentHashMap <Long , AtomicInteger > remainingSlotsByRegion =
95+ loadRemainingSlots (collectionRequests );
9396
9497 String restaurantNames = restaurantRepository .findAll ().stream ()
95- .map (com . yogieat . restaurant . domain . Restaurant ::name )
98+ .map (Restaurant ::name )
9699 .collect (Collectors .joining (", " ));
97100
98101 Map <LocationCategoryKey , List <SuggestionRestaurant >> allSuggestions =
99- geminiClient . generateRestaurantsBatch ( locationsToCollect , FOOD_CATEGORIES , restaurantNames , RESTAURANTS_PER_REQUEST );
102+ generateBatchSuggestions ( collectionRequests , restaurantNames );
100103
101104 AtomicInteger totalProcessed = new AtomicInteger (0 );
102105 AtomicInteger totalSuccess = new AtomicInteger (0 );
@@ -112,21 +115,21 @@ public void collectAllRegions() {
112115 LocationCategoryKey key = entry .getKey ();
113116 List <SuggestionRestaurant > suggestions = entry .getValue ();
114117
115- RegionSummary regionSummary = regionByLocation .get (key .location ());
116- if (regionSummary == null || isRegionLimitReached (regionSummary .region (), regionCounts )) {
118+ RestaurantCollectionPlan .Request request = requestByLocation .get (key .location ());
119+ if (request == null ) {
120+ continue ;
121+ }
122+
123+ RegionMaster region = request .regionSummary ().region ();
124+ AtomicInteger remainingSlots = remainingSlotsByRegion .get (region .id ());
125+ if (remainingSlots == null || remainingSlots .get () <= 0 ) {
117126 continue ;
118127 }
119128
120129 futures .add (executor .submit (() -> {
121130 try {
122- int processed = processRestaurantsForRegion (regionSummary . region () , key .category (), suggestions );
131+ int processed = processRestaurantsForRegion (region , key .category (), suggestions , remainingSlots );
123132 totalProcessed .addAndGet (processed );
124- if (processed > 0 ) {
125- regionCounts .compute (
126- regionSummary .region ().id (),
127- (regionId , count ) -> count == null ? (long ) processed : count + processed
128- );
129- }
130133 totalSuccess .incrementAndGet ();
131134 } catch (Exception e ) {
132135 log .error ("Failed: {} - {}" , key .location (), key .category (), e );
@@ -158,42 +161,50 @@ public void collectAllRegions() {
158161 }
159162 }
160163
161- private Map <Long , Long > loadRegionCounts (List <RegionSummary > activeRegions ) {
162- return activeRegions .stream ()
164+ private void logCollectionPlan (List <RestaurantCollectionPlan .Request > collectionRequests ) {
165+ collectionRequests .forEach (request -> log .info (
166+ "Region {} needs collection: {}/{} restaurants, requesting {} per category" ,
167+ request .regionSummary ().region ().displayName (),
168+ request .regionSummary ().restaurantCount (),
169+ RestaurantCollectionPlan .REGION_RESTAURANT_LIMIT ,
170+ request .countPerCategory ()
171+ ));
172+ }
173+
174+ private ConcurrentHashMap <Long , AtomicInteger > loadRemainingSlots (
175+ List <RestaurantCollectionPlan .Request > collectionRequests
176+ ) {
177+ return collectionRequests .stream ()
163178 .collect (Collectors .toMap (
164- summary -> summary .region ().id (),
165- RegionSummary ::restaurantCount
179+ request -> request .regionSummary ().region ().id (),
180+ request -> new AtomicInteger (request .remainingSlots ()),
181+ (existing , ignored ) -> existing ,
182+ ConcurrentHashMap ::new
166183 ));
167184 }
168185
169- private List <RegionSummary > filterRegionsNeedingCollection (
170- List <RegionSummary > activeRegions ,
171- Map < Long , Long > regionCounts
186+ private Map < LocationCategoryKey , List <SuggestionRestaurant >> generateBatchSuggestions (
187+ List <RestaurantCollectionPlan . Request > collectionRequests ,
188+ String restaurantNames
172189 ) {
173- List <RegionSummary > regionsToCollect = new ArrayList <>();
174-
175- for (RegionSummary summary : activeRegions ) {
176- RegionMaster region = summary .region ();
177- long currentCount = regionCounts .getOrDefault (region .id (), 0L );
178- int limit = getRegionLimit (region );
179-
180- if (currentCount < limit ) {
181- regionsToCollect .add (summary );
182- log .info ("Region {} needs collection: {}/{} restaurants" ,
183- region .displayName (), currentCount , limit );
184- } else {
185- log .info ("Region {} reached limit: {}/{} restaurants (skipping)" ,
186- region .displayName (), currentCount , limit );
187- }
188- }
190+ Map <LocationCategoryKey , List <SuggestionRestaurant >> suggestions = new HashMap <>();
191+ Map <Integer , List <RestaurantCollectionPlan .Request >> requestsByCount = collectionRequests .stream ()
192+ .collect (Collectors .groupingBy (RestaurantCollectionPlan .Request ::countPerCategory ));
189193
190- return regionsToCollect ;
191- }
194+ requestsByCount .forEach ((countPerCategory , requests ) -> {
195+ List <String > locationsToCollect = requests .stream ()
196+ .map (request -> request .regionSummary ().region ().displayName ())
197+ .toList ();
198+
199+ suggestions .putAll (geminiClient .generateRestaurantsBatch (
200+ locationsToCollect ,
201+ FOOD_CATEGORIES ,
202+ restaurantNames ,
203+ countPerCategory
204+ ));
205+ });
192206
193- private boolean isRegionLimitReached (RegionMaster region , Map <Long , Long > regionCounts ) {
194- long currentCount = regionCounts .getOrDefault (region .id (), 0L );
195- int limit = getRegionLimit (region );
196- return currentCount >= limit ;
207+ return suggestions ;
197208 }
198209
199210 /**
@@ -220,6 +231,15 @@ public int processRestaurantsForRegion(
220231 RegionMaster region ,
221232 String category ,
222233 List <SuggestionRestaurant > suggestions
234+ ) {
235+ return processRestaurantsForRegion (region , category , suggestions , new AtomicInteger (suggestions .size ()));
236+ }
237+
238+ private int processRestaurantsForRegion (
239+ RegionMaster region ,
240+ String category ,
241+ List <SuggestionRestaurant > suggestions ,
242+ AtomicInteger remainingSlots
223243 ) {
224244 RestaurantValidator .ValidationContext validationContext =
225245 restaurantCollectionWriteService .prepareValidationContext (region );
@@ -232,7 +252,7 @@ public int processRestaurantsForRegion(
232252 for (SuggestionRestaurant suggestion : suggestions ) {
233253 futures .add (executor .submit (() -> {
234254 processSingleSuggestion (
235- suggestion , region , category , validationContext , savedCount
255+ suggestion , region , category , validationContext , savedCount , remainingSlots
236256 );
237257 return null ;
238258 }));
@@ -259,8 +279,13 @@ private void processSingleSuggestion(
259279 RegionMaster region ,
260280 String category ,
261281 RestaurantValidator .ValidationContext validationContext ,
262- AtomicInteger savedCount
282+ AtomicInteger savedCount ,
283+ AtomicInteger remainingSlots
263284 ) {
285+ if (remainingSlots .get () <= 0 ) {
286+ return ;
287+ }
288+
264289 RestaurantEnrichedData enrichedData ;
265290 try {
266291 kakaoApiSemaphore .acquire ();
@@ -298,6 +323,10 @@ private void processSingleSuggestion(
298323 return ;
299324 }
300325
326+ if (!tryAcquireSlot (remainingSlots )) {
327+ return ;
328+ }
329+
301330 boolean saved = restaurantCollectionWriteService .persistRestaurant (
302331 suggestion ,
303332 region ,
@@ -308,17 +337,28 @@ private void processSingleSuggestion(
308337 );
309338 if (saved ) {
310339 savedCount .incrementAndGet ();
340+ } else {
341+ releaseSlot (remainingSlots );
311342 }
312343 }
313344
314- private int getRegionLimit (RegionMaster region ) {
315- if (region != null && HIGH_VOLUME_REGION_CODES .contains (region .code ())) {
316- return 100 ;
345+ private boolean tryAcquireSlot (AtomicInteger remainingSlots ) {
346+ while (true ) {
347+ int current = remainingSlots .get ();
348+ if (current <= 0 ) {
349+ return false ;
350+ }
351+ if (remainingSlots .compareAndSet (current , current - 1 )) {
352+ return true ;
353+ }
317354 }
318- return DEFAULT_REGION_LIMIT ;
319355 }
320356
321- private BinaryOperator <RegionSummary > keepExistingRegion () {
357+ private void releaseSlot (AtomicInteger remainingSlots ) {
358+ remainingSlots .incrementAndGet ();
359+ }
360+
361+ private BinaryOperator <RestaurantCollectionPlan .Request > keepExistingRequest () {
322362 return (existing , ignored ) -> existing ;
323363 }
324364
0 commit comments