I would like to introduce "Sosohaeng (小小行): Small-Town Travel Destination Recommendation Service Using RAG," developed in the Capstone Design & Entrepreneurship Project at Ewha Womans University.

Introduction
Our team consists of three members, all majoring in Computer Science and Engineering (Class of '22).
While everyone contributed evenly across frontend, backend, and AI feature implementation, each member took ownership of specific core components.
I was responsible for leading the overall 'Local Specialty Market Feature' and designing the 'RAG-Based Travel Destination Recommendation LLM Prompts.'
Our project was initially inspired by the viral social media trend of taking spontaneous trips: "Throwing a pen at a map and traveling to wherever the point lands."
Recent travel trends are shifting from "checking off famous tourist hotspots" to "small-town travel to discover personal preferences." However, existing search portals and map apps provide only fragmented information, failing to resolve complex user desires at once—such as "I want to walk along a quiet forest path, where should I go?"
Furthermore, small business owners in regional small towns face significant challenges in connecting with tourists due to digital marketing limitations, despite offering high-quality local products.
To address both travelers' search fatigue and the issue of regional economic imbalance, our team, 'Naega-Green,'planned and developed 'Sosohaeng'—an all-in-one platform combining AI-driven hyper-personalized recommendations with local commerce.
Consequently, Sosohaeng is not merely a travel information app, but an integrated platform that seamlessly weaves together LLM + RAG + Location-Based Recommendations + Local Market. This blog post focuses particularly on the core technology of this project—the RAG-based travel recommendation pipeline—as well as the backend and frontend implementation of the surrounding festival and market features.
Project Overview
Problem Definition (Pain Points)
To clearly articulate the necessity of our project, we began with a detailed analysis of user pain points.
Pain Point 1 – Highly Fragmented Information
When planning a trip, travelers navigate across multiple platforms—including Instagram, YouTube, blogs, and search portals—to gather information. The underlying issues are:
- Information is fragmented across disparate platforms.
- It is difficult to distinguish sponsored advertisements from genuine user reviews.
- Content algorithms prioritize "frequently viewed destinations" rather than "personalized, user-tailored spots."
Consequently, travelers experience significant search fatigue.
Pain Point 2 – Small Towns and Local Business Districts are Consistently Overlooked
Most travel services heavily feature content centered around already popular tourist destinations such as Seoul, Jeju, and Busan. Conversely:
- Small and medium-sized towns rarely surface in search query results.
- Stories behind local specialties and small business owners rarely gain visibility on mainstream platforms.
Even when travelers visit small towns, the connection with the local community ends as soon as the trip concludes.
In summary, our project originated from the core pain points of two key stakeholder groups: Travelers and Local Small Businesses.
- Inefficient Information Search (Travelers): Exhaustion caused by manually cross-referencing sponsored ads versus factual details across Instagram, blogs, and map applications.
- Regional Tourism Imbalance (Region): Tourist concentration in major metropolitan hubs (such as Seoul, Jeju, and Busan), overshadowing the unique charm of smaller towns.
- Lack of Digital Sales Channels (Local Sellers): A absence of online sales and marketing channels for local sellers to establish continuous touchpoints with visiting tourists.
Solution
To address these issues, we adopted Retrieval-Augmented Generation (RAG) architecture. Rather than simply presenting static lists of destinations, our AI interprets user intent and provides conversational recommendations grounded in verified data from the Korea Tourism Organization (TourAPI). Furthermore, by seamlessly integrating a local market feature where users can purchase regional specialties directly from recommended areas, we engineered a virtuous cycle that promotes local economic growth.
Project Details
To solve both problems simultaneously, Sosohaeng integrated the following three core features into a single application:
- RAG-based AI Travel Destination Recommendation (Main): Identifies user preferences (companions, travel style, timing, etc.) through conversational interactions and suggests tailored small-town travel itineraries. Moving beyond simple keyword matching, the LLM understands context and provides evidence-based reasoning for recommendations grounded in our actual DB data.
- LBS-based Real-time Festival Information: Leverages Location-Based Services (LBS) to deliver real-time information on ongoing festivals within a 10 km radius of the user's current location.
- Local Coexistence Specialty Market: Curates and displays local specialties and souvenirs sold by small businesses in the recommended areas to convert travel intent into actual consumption. It features a message board-style Q&A system to establish a direct communication channel between buyers and sellers.
Among these, the most critical component is Feature #1: RAG-based AI Travel Destination Recommendation.
1) Travel Destination Recommendation Feature – Core RAG + Prompt Design
Travel destination recommendation in Sosohaeng goes beyond a simple "Query LLM Answer" architecture.
Its pipeline strictly aligns with Retrieval-Augmented Generation (RAG):
1. User Profile & Query Analysis
- Accumulates criteria such as party size, age group, companion dynamic, budget, season, preferred transit, and travel style throughout the conversation.
- Dynamically updates state in an interactive manner while maintaining variables such as current_profile and turn_count.
2. Keyword Expansion (Retrieval Preparation Step)
- Expands vague user queries like "a good autumn destination for nature healing" into searchable keyword sets such as [peaceful stroll, quiet lake, small-town vibe].
- Utilizes these expanded keywords to query and retrieve candidate destinations from TourAPI + internal DB (RecommendTourInfo).
3. Data Verification (Retrieval Step)
- Filters retrieved candidates against strict validation criteria:
- Verifies whether the location qualifies as an actual 'small town'.
- Checks suitability for the specific timeframe (weather, ongoing festival seasons, etc.).
- Confirms the existence of valid coordinates, addresses, and image URLs in the DB.
Data structure during this phase uses schemas such as TourInfoOut:
# Excerpt from app/models/recommend_models.py
class TourInfoBase(BaseModel):
contentid: str
contenttypeid: Optional[str]
title: str
addr1: Optional[str]
addr2: Optional[str]
zipcode: Optional[str]
areacode: Optional[str]
sigungucode: Optional[str]
cat1: Optional[str]
cat2: Optional[str]
cat3: Optional[str]
tel: Optional[str]
firstimage: Optional[str]
firstimage2: Optional[str]
class TourInfoOut(TourInfoBase):
# Schema for output
pass
4. LLM Generation Stage + Structured Output
- The system is designed to generate both response (explanatory text) and recommendations (structured recommendation list) simultaneously.
To achieve this, we defined a Pydantic schema named ChatRecommendResponse.
# Excerpt from BE/app/schemas.py
class ChatbotRequest(BaseModel):
message: str = Field(..., description="User's message sent to the chatbot.")
class ChatRecommendResponse(BaseModel):
response: str = Field(..., description="Chatbot's text response.")
recommendations: List[TourInfoOut] = Field(
[],
description="List of recommended DB-based destinations."
)
Based on this schema, the backend extracts two components from the LLM output:
- Natural language explanation
- DB-verified travel destination list
5. Profile Updates and Follow-up Questions in a Single Pass
Sosohaeng's prompt design requires the LLM to generate all of the following in a single pass:
- Accumulated user profile up to the current turn (current_profile)
- The follow-up question the chatbot will ask next (next_question)
- Structured recommendation list
The LLM output includes JSON wrapped in special markers as follows:
(Natural language explanation...)
---PROFILE_UPDATE---
{ ... current_profile, turn_count, next_question ... }
---END_PROFILE---
The frontend parses and separates the text and JSON based on these markers.
// Excerpt from screens/ChatbotRecommend.js
const payload = JSON.stringify({
message: userMessage,
current_profile: currentProfile,
turn_count: turnCount,
});
const apiResponse = await sendChatbotMessage(payload);
if (apiResponse && apiResponse.response) {
const rawResponse = apiResponse.response;
recommendations = apiResponse.recommendations || [];
const profileMarkerStart =
rawResponse.indexOf('---PROFILE_UPDATE---');
const profileMarkerEnd = rawResponse.indexOf('---END_PROFILE---');
if (profileMarkerStart !== -1 && profileMarkerEnd !== -1) {
const jsonStart =
profileMarkerStart + '---PROFILE_UPDATE---'.length;
const jsonEnd = profileMarkerEnd;
const jsonString = rawResponse
.substring(jsonStart, jsonEnd)
.trim();
const parsedData = JSON.parse(jsonString);
setCurrentProfile(parsedData.current_profile || {});
setTurnCount(parsedData.turn_count || 0);
botResponseText =
parsedData.next_question ||
rawResponse.substring(0, profileMarkerStart).trim();
} else {
botResponseText = rawResponse;
setCurrentProfile({});
setTurnCount(0);
}
}
Thanks to this architecture, the entire pipeline—"Interactive survey Profile accumulation Customized recommendation Next question"—operates seamlessly within a single unified flow.
Prompt Structure and Design Intent
>> PROMPT <<
[Role]
You are an expert curator for small and medium-sized town travel in Korea.
Recommend {num} travel destinations reflecting all requirements below.
[Requirements]
- Target: Age {age}, Companion: {relation}, Group Size: {people} people
- Duration: {period}, Season/Timing: {when}, Transportation: {transportation}
- Preferred Style: {style}
- Budget: {budget}
- Other: {etc}
[Generation Rules]
1. Include at least 1 small town, preferably multiple. Sort results prioritizing small towns.
2. Reflect recent trends (past 120 days).
3. For each candidate:
- "Why now?": 1 sentence on trend/seasonality
- "What to do": 1 sentence on core activity/itinerary, concise route
- "Requirement Fit": 1 sentence mapping user constraints
4. Include RAG grounds/sources (Document name/summary/optional URL).
5. Respect transportation & duration constraints (e.g., 2-day route for a 1-night 2-day trip;
public transit vs. private car).
6. Consider cost-effectiveness within budget (rough breakdown for admission, activities,
transit, dining).
7. If uncertain, state "Insufficient data" and present a safe alternative.
8. Tone: Polite (honorifics), non-exaggerated, concise.
[Keyword Expansion]
Provide 2-3 expanded keywords reflecting user intent (e.g., 'beach, sunset, cable car').
[Output Format - Must be strictly followed]
1. Summary Section (Natural Language)
"You prefer a trip tailored to {Summary of main requirements}! Keywords were expanded
to '{Expanded Keyword 1}', '{Expanded Keyword 2}', '{Optional Expanded Keyword 3}'
to match your criteria.
Here are {recommend} recommended travel destinations perfectly suited for you."
2. Output (JSON Schema)
{ "results": [...], "validation_checklist": {...} }
3. Final One-Line Notice:
"※ Some information may vary depending on operational circumstances, so please check
the latest guidance before visiting."
Sosohaeng’s travel destination recommendation does not operate on a casual "Please handle this well, LLM" instruction; it runs on top of a strict prompt specification.
The prompt is broadly divided into 5 layers: Role – Inputs – Policies – Keyword Expansion – Output Schema.
1. Role
- Permanently assigns the role of "Expert Curator for Small and Medium-Sized Town Travel in Korea" to the LLM.
- Prompts the model to think like a domain expert with a small-town filter rather than providing generic "national tourist info."
2. Inputs
- Passes parameters like {age}, {relation}, {people}, {period}, {when}, {transportation}, {style}, {budget}, and {etc} in a structured format.
- These parameters connect to the user profile built up during conversation and are re-verified in the validation_checklist.
3. Policies
- Strongly enforces domain rules at the prompt level: "Include at least 1 small town / Prioritize small towns", "Reflect trends from the past 120 days", "Mandatorily reflect transit/duration/budget constraints", and "If uncertain, state 'Insufficient data' and suggest alternatives."
- The 3-sentence structure (Why now? / What to do / Requirement fit) is rendered directly as cards in the frontend UI.
4. Keyword Expansion Layer
- Asks the LLM to condense user natural language requests ("I want a quiet, healing beach in autumn") into 2-3 expanded keywords like beach / sunset / cable car.
- These keywords serve as retrieval queries in the RAG stage and expose the model's Chain-of-Thought reasoning process to the user.
5. Output Schema
- Stage 1: Natural language summary section (Greeting + Requirements Summary + Keyword Introduction).
- Stage 2: JSON schema formatted as {"results": [...], "validation_checklist": {...}}.
- Stage 3: Fixed one-line disclaimer notice.
*Enforcing this structure allows the backend to immediately parse the LLM output and map it to UI components.
*The validation_checklist summarizes whether small-town conditions, budget/transit constraints, and data sufficiency are met, acting as self-verification metadata.
Thanks to this architecture, Sosohaeng’s prompt effectively functions as a domain-specific API specification.
The LLM is constrained to act as a "small-town biased travel curator + verifiable recommendation engine" rather than writing unconstrained text.
Advantages of This Prompt Design
This structured prompt architecture provides the following key advantages:
1. Seamless Domain Constraint Integration
- Explicit rules such as "Include at least 1 small town", "Reflect trends from the past 120 days", and "Respect transportation/budget constraints" significantly prevent the LLM from defaulting to generic, famous tourist hotspots.
- As a result, a recommendation engine with an intentional 'small-town bias' is effectively implemented directly at the prompt level.
2. Native Alignment with RAG Architecture
- Keyword expansion, RAG source attribution (sources), and the validation_checklist all reinforce the principle to "recommend strictly within actual DB data."
- Rather than hallucinating non-existent places, the model is guided to focus its capacity on sorting, filtering, and explaining candidate destinations supplied during the Retrieval phase.
3. Streamlined FE/BE Integration
- Enforcing a strict JSON output schema allows the backend to easily parse results and validation_checklist directly into objects.
- The frontend can seamlessly map the three-sentence structure (Why now? / What to do / Requirement fit) straight into UI card components.
- In short, the LLM response is treated as structured data rather than an unformatted block of text.
4. Consistent User Experience (UX)
- Regardless of input variations, responses consistently follow the structure: [Summary Greeting Card-style Recommendation List (3-sentence structure) Closing Notice].
- Users receive responses in a predictable, familiar format, directly contributing to the trust and reliability crucial for travel services.
5. Enforces Safe and Honest AI Behavior
- The rule to "state 'Insufficient data' when uncertain and present safe alternatives" reduces instances where the model confidently asserts ambiguous information.
- This induces an honest failure mode, guiding the model to generate transparent responses such as "Due to a lack of recent data in this region, recommending a nearby small town instead."
Why RAG Was Mandatory

It is entirely possible to build a travel chatbot using an unaugmented LLM alone. However, doing so immediately reveals severe limitations.
During the early stages of the project, relying solely on standard LLMs (such as vanilla ChatGPT) introduced critical issues:
- Hallucinations: High risk of generating factual errors, such as recommending non-existent travel spots or guiding users to permanently closed businesses.
- Lack of Freshness: Inability to reflect newly opened attractions or current festival information established after the LLM's knowledge cutoff date.
- Inaccurate Details: Misrepresenting period, location, or accessibility information even for real, existing places.
- Constraint Failures: Struggling to strictly enforce domain-specific rules such as prioritizing "small towns."
To resolve these issues, we adopted a Retrieval-Augmented Generation (RAG) architecture. We embedded reliable, up-to-date tourism data collected via the Korea Tourism Organization (TourAPI) into a vector database (FAISS). Upon receiving a user query, relevant data is retrieved and supplied to the LLM with explicit instructions: "Answer strictly based on this context."
Consequently, Sosohaeng operates on the following 3-stage pipeline:
1. Retrieval Stage
- Fetches candidate destinations from TourAPI + internal DB filtered by region code, sigungu code, categories (cat1~cat3), and seasonal timeframes.
2. Augmentation Stage
- Supplies key attributes (title, addr1, firstimage, categories, coordinates) of retrieved candidates to the LLM as explicit context.
3. Generation Stage
- The LLM generates recommendations, summaries, and explanations solely within the bounds of this context.
- The prompt explicitly enforces rules such as "Do not fabricate locations absent from the DB" and "State 'Difficult to recommend' honestly if data is insufficient."
Thanks to this architecture, Sosohaeng's recommendations are grounded in real small towns and verified destinations existing in our DB rather than "AI-hallucinated places."
This is precisely why we committed to a RAG-based approach.
2) Festival Information Feature – Haversine-based Nearby Festival Recommendation
The second core feature allows users to explore nearby festivals based on their real-time location.
[Location-Based Distance Calculation]
The backend defines a custom distance calculation column in the Festival ORM model using the Haversine formula.
# Excerpt from app/models/festival_models.py
class Festival(Base):
# ... id, title, mapx (longitude), mapy (latitude) definitions ...
@classmethod
def distance_col(cls, user_lat: float, user_lon: float):
"""
SQLAlchemy expression calculating distance from user location to festival venues
(cls.mapy: latitude, cls.mapx: longitude)
"""
R_KM = 6371.0
return (
R_KM * func.acos(
func.cos(func.radians(user_lat)) * func.cos(func.radians(cls.mapy))
* func.cos(func.radians(cls.mapx) - func.radians(user_lon))
+ func.sin(func.radians(user_lat)) * func.sin(func.radians(cls.mapy))
)
).label('distance')
The service layer leverages this column to perform distance-based filtering and sorting.
# Excerpt from app/services/festival_services.py
def list_festivals(
db: Session,
user_lat: Optional[float] = None,
user_lon: Optional[float] = None,
distance_km: Optional[float] = None,
page: int = 1,
size: int = 20,
order_by: str = "distance",
):
query = db.query(Festival)
# Exclude expired festivals
# ...
distance_col = None
if user_lat is not None and user_lon is not None:
distance_col = Festival.distance_col(user_lat, user_lon)
query = query.add_columns(distance_col)
if distance_km is not None and distance_km > 0:
query = query.filter(distance_col <= distance_km)
if order_by == 'distance' and distance_col is not None:
query = query.order_by(distance_col.asc())
elif order_by == 'title':
query = query.order_by(Festival.title.asc())
else:
query = query.order_by(Festival.id.asc())
total_count = query.count()
items = query.offset((page - 1) * size).limit(size).all()
return items, total_count
The frontend retrieves current user coordinates via expo-location and invokes this API:
// Excerpt from screens/FestivalsScreen.js
const { data, isLoading } = useQuery({
queryKey: ['festivals', userLocation, viewMode],
queryFn: () =>
fetchFestivals({
user_lat: userLocation?.latitude,
user_lon: userLocation?.longitude,
distance_km: 10, // 10km radius
order_by: 'distance',
page: 1,
size: 50,
}),
enabled: !!userLocation,
});
Through this pipeline, users can explore ongoing festivals within an radius of their current position across interactive map and list views.
3) Local Specialty Market Feature – Extending Travel Experiences to Local Commerce

The third feature is a local specialty market directly linked to travel destinations.
Rather than a basic shopping app, it is designed so that:
- Products are linked to travel destinations via regional codes.
- Buyers and sellers communicate via product Q&A.
- Users can save items to their wishlists for future purchases.
<Key Frontend Files for Market Feature>
The market feature required a large number of UI screens:
- MarketHome.js: The initial entry screen for the market section.
- ProductDetailScreen.js: Detailed product page opened when a user selects an item.
- ProductQnaScreen.js: Q&A board screen accessed via the inquiry button on the product detail page.
- ProductCreateScreen.js: Screen accessed via the product registration button on Market Home.
- WishlistScreen.js: Screen collecting favorited/bookmarked items.

<Key Backend Files for Market Feature>
In the backend, files are organized across models, router, and service layers alongside other platform capabilities.



[Product Listing Query – Handling Region, Search, and Sorting Conditions Simultaneously]
The list_products function in MarketService handles the following operational requirements:
- Search across title, summary, and location name.
- Region filtering (region).
- Sorting by popularity (likes), reviews (rating), or recency.
- Pagination.
# Excerpt from app/services/market_service.py
def list_products(
db: Session,
q: Optional[str] = None,
region: Optional[str] = None,
sort: str = "likes",
page: int = 1,
size: int = 20,
) -> Tuple[List[MarketProduct], int]:
stmt = (
select(MarketProduct)
.options(
joinedload(MarketProduct.images), # Eager load images
joinedload(MarketProduct.seller), # Eager load seller info
)
)
conds = []
if q:
like = f"%{q}%"
conds.append(or_(
MarketProduct.title.ilike(like),
MarketProduct.summary.ilike(like),
MarketProduct.location.ilike(like),
))
if region and region != '전체':
conds.append(MarketProduct.region == region)
if conds:
stmt = stmt.where(and_(*conds))
if sort == '인기순':
stmt = stmt.order_by(desc(MarketProduct.likes), desc(MarketProduct.rating))
elif sort == '후기순':
stmt = stmt.order_by(desc(MarketProduct.rating), desc(MarketProduct.likes))
else: # Recency
stmt = stmt.order_by(desc(MarketProduct.created_at))
total = db.scalar(select(func.count()).select_from(stmt.subquery()))
rows = db.execute(
stmt.offset((page - 1) * size).limit(size)
).scalars().unique().all()
return rows, (total or 0)
The frontend calls this API to render the Market Home UI:
// Excerpt from screens/MarketHome.js
const load = async () => {
setLoading(true);
try {
const params = new URLSearchParams();
if (search) params.append('q', search);
if (selectedRegion && selectedRegion !== '전체') {
params.append('region', selectedRegion);
}
params.append('sort', sortLabel); // 'Popularity', 'Reviews', 'Recency'
const res = await fetch(`${API_BASE_URL}/market/products?${params.toString()}`);
const json = await res.json();
setProducts(json.items || []);
} finally {
setLoading(false);
}
};
Product registration, details, and Q&A screens are implemented through RESTful communication with FastAPI:
- Image uploads are processed using FormData.
- Q&A operations are managed via dedicated endpoints linked to the MarketQna table.
Development Details
System Architecture
- Frontend
- React Native (Expo)
- Key Screens: HomeScreen, ChatbotRecommend, FestivalsScreen, MarketHome, ProductDetail, ProductQnAScreen, etc.
- Backend
- FastAPI
- Routers: recommend_router, festival_router, market_router, etc.
- Service Layer: recommend_service, festival_services, market_service.
- External API: Korea Tourism Organization TourAPI (tour_api_client, tour_api_service).
- Database
- MySQL (AWS RDS), SQLAlchemy ORM.
- AI / RAG
- Receives ChatbotRequest at the chatbot endpoint.
- Inside recommend_service:
- Keyword expansion based on user profile and query.
- Relevant destination Retrieval from DB / TourAPI.
- LLM invocation Parses response into ChatRecommendResponse.
- Output schema (JSON + custom delimiters) strictly enforced at the prompt level.
Software Architecture Diagram

How to Run Sosohaeng
[1] Open terminal and install required packages:
pip install passlib
pip install python-jose
pip install pymysql
[2] Navigate to BE directory, activate virtual environment, and launch server & DB:
cd BE
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
[3] Update local IP configurations under `FE/sosohaeng-app/src/config`:
1. In api.js, set the MY_MAC_IP variable to your current network IP address.
2. In client.js, set the FINAL_BASE_URL variable to your current network IP address.
[4] Open a new terminal instance, navigate to FE, and run ExpoGo via QR Code:
cd FE/sosohaeng-app
npx expo start -c --lan
Technical Differentiator
RAG Pipeline Implementation Process
Step 1: Keyword Expansion and Intent Recognition
Expands abstract user queries like "I want to heal" into concrete, searchable keywords such as forest path, stroll, quiet. The prompt engineering phase was specifically designed to analyze user personas and real-time interaction contexts (Turn Count) to achieve this.
Step 2: Data Verification and Prompt Injection
Injects retrieved destination details (Context) into the LLM prompt. Rather than generating unstructured text, a strict JSON schema was enforced to ensure reliable card visualization on the mobile UI.
Below is an excerpt of the response schema defined in schemas.py. Clear separation between AI response text and recommendation list data guarantees frontend rendering stability.
# Excerpt from BE/app/schemas.py
class ChatRecommendResponse(BaseModel):
response: str = Field(..., description="Chatbot's text response.")
recommendations: List[TourInfoOut] = Field([], description="List of recommended DB-based destinations.")
Step 3: Service Logic Wiring
The actual router (recommend_router.py) receives user messages, triggers the AI service pipeline, and returns structured data to the client.
# Excerpt from BE/app/router/recommend_router.py
@router.post("/chatbot", summary="RAG-based AI Chatbot Recommendation", response_model=ChatRecommendResponse)
async def chatbot_endpoint(request: ChatbotRequest):
try:
# Call RAG service function (Extract keywords -> Search DB -> Generate response)
result = await get_chatbot_search_keywords_and_recommendations(request.message)
return ChatRecommendResponse(
response=result["ai_response_text"],
recommendations=result["db_recommendations"]
)
except HTTPException as e:
raise e
LBS-based Festival Distance Calculation Optimization
Festival features require 'Nearby' spatial intelligence rather than static listings. We embedded distance calculation logic directly onto the Festival model using the Haversine formula (Spherical Law of Cosines).
Computing distances at the application layer requires fetching all database records, causing significant performance bottlenecks. To optimize performance, we used SQLAlchemy's func module to perform distance calculations and sorting directly at the database level.
# Excerpt from BE/app/models/festival_models.py
@classmethod
def distance_col(cls, user_lat: float, user_lon: float):
"""
Returns a SQLAlchemy expression that calculates distance from user location
(user_lat, user_lon) to festival venues at the DB level
"""
R_KM = 6371.0 # Earth's radius in km
return (
R_KM * func.acos(
func.cos(func.radians(user_lat)) * func.cos(func.radians(cls.mapy))
* func.cos(func.radians(cls.mapx) - func.radians(user_lon))
+ func.sin(func.radians(user_lat)) * func.sin(func.radians(cls.mapy))
)
).label('distance')
Utilized within festival_services.py, this query-level filter executes instantly when a user applies distance filters (e.g., within 10km).
# Excerpt from BE/app/services/festival_services.py
if user_lat is not None and user_lon is not None:
distance_col = Festival.distance_col(user_lat, user_lon)
query = query.add_columns(distance_col)
# Distance filtering (e.g., within 10km)
if distance_km is not None and distance_km > 0:
query = query.filter(distance_col <= distance_km)
Data Consistency & Relationship Management (ORM)
The Local Market feature involves complex relationships across Products, Sellers (User), Images, and Inquiries (QnA).
To ensure efficient queries and prevent N+1 query issues, we strategically combined SQLAlchemy Eager Loading techniques (joinedload and selectinload).
# Excerpt from BE/app/services/market_service.py
def get_product(db: Session, product_id: int) -> Optional[MarketProduct]:
stmt = (
select(MarketProduct)
.where(MarketProduct.id == product_id)
.options(
selectinload(MarketProduct.images), # selectinload is efficient for 1:N relationships
selectinload(MarketProduct.qna_list).joinedload(MarketQna.author),
joinedload(MarketProduct.seller) # joinedload fetches N:1 relationships in a single join
)
)
return db.execute(stmt).scalar_one_or_none()
Key Learnings & Achievements
Through this project, I achieved significant technical growth across the following areas:
- Integrating AI with Real-world Data: I realized that LLMs are not omnipotent and that building a 'controllable system' like RAG is essential for production services. Enforcing strict JSON schemas to eliminate frontend rendering errors particularly highlighted the importance of robust prompt engineering.
- Asynchronous Data Processing: I built an asynchronous data ingestion pipeline using AsyncSession and httpx to regularly collect and store large volumes of data from the Korea Tourism Organization (TourAPI), strengthening my big data processing capabilities.
- Spatial Query Optimization: While implementing location-based services (LBS), I moved beyond storing plain floating-point coordinates and learned how to optimize spatial queries using database-level functions.
[Key Technical Insights]
1. Directly Experienced the Limitations of Standalone LLM Services
- When building an early Proof of Concept (PoC) using an unaugmented LLM, the model frequently hallucinated non-existent destinations/festivals and confused facts with DB data.
- This hands-on experience clearly demonstrated the necessity of a RAG architecture.
2. Prompt Design Functions Analogously to 'API Design'
- Allowing LLMs to generate unconstrained text makes structured parsing difficult.
- By locking output formats to a JSON schema and introducing explicit delimiters like ---PROFILE_UPDATE---, both the frontend and backend could interface with the LLM as a predictable API endpoint.
3. Explicit Domain-Constrained RAG is Critical
- Enforcing domain rules—such as "Include at least one small town", "State 'Insufficient Data' honestly", and "Do not recommend places absent from TourAPI/DB"—across both prompt specifications and service logic provided deep insight into bridging domain expertise with AI architectures.
4. Combining LBS with Commerce Transforms End-to-End Travel Experiences
- Integrating real-time location-based festival recommendations with local specialty commerce inside a single app enabled us to architect a unified user journey: Trip Planning On-site Experience Converting Travel Memories into Local Consumption.
Conclusion
Sosohaeng aims to go beyond a conventional travel guide by using technology to resolve regional information inequality and unlock new business opportunities for local sellers.
Although this capstone project is ongoing, it successfully unifies three core pillars—AI Travel Recommendations, Location-based Festival Discovery, and Local Specialty Markets—into a single user experience.
This post focused on the technical implementation of our RAG-based travel recommendation pipeline, prompt architecture, and the surrounding festival and commerce features.
Future Roadmap:
- Vector Search Integration: Introduce vector similarity search into the RAG retrieval layer to enhance semantic travel spot recommendations.
- Cross-Domain Travel-Commerce Engine: Link market purchase data with destination preference data to recommend local items tailored to user travel profiles (e.g., "Travelers who enjoyed this destination also purchased these local products").
We will continue to refine our recommendation algorithms based on user feedback and integrate payment processing systems to enhance platform maturity. Through RAG technology, we look forward to providing travelers with 'foolproof trips' while injecting 'economic vitality' into local communities.
References
- Korea Tourism Organization TourAPI 4.0 https://api.visitkorea.or.kr
- FastAPI Official Documentation https://fastapi.tiangolo.com
- React Native / Expo Official Documentation
https://reactnative.dev
https://docs.expo.dev - OpenAI API Documentation & RAG Best Practices
- Main Doc: https://platform.openai.com/docs
- API Reference: https://platform.openai.com/docs/api-reference
'주전공 > 캡스톤디자인과창업프로젝트' 카테고리의 다른 글
| [이화여대 캡스톤디자인과창업프로젝트] 기술블로그 | 소소행 : RAG를 활용한 소도시 여행지 추천 서비스 (0) | 2025.11.24 |
|---|---|
| 그로쓰 기록(1) : 마켓 기능 구축 및 장바구니 & 찜하기 확장 (0) | 2025.10.02 |
| 여름방학 기록(7): DB 마이그레이션 체계(Alembic) 도입 및 초기 스키마 생성 (0) | 2025.08.30 |
| 여름방학 기록(6): 데이터베이스 초기화 | DB Schema 생성 (0) | 2025.08.24 |
| 여름방학 기록(5): FastAPI 백엔드 기본 개발환경 세팅 및 서버 띄우기 | 코드 실행 및 연동 1차 테스트 + Mock 실행 확인 (0) | 2025.08.18 |