Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ db.sqlite3
# ๋กœ๊ทธํŒŒ์ผ ๋ฌด์‹œ
*.log
/logs
nginx.conf
nginx.conf
*.sqlite3
2 changes: 1 addition & 1 deletion config/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
})
})
2 changes: 1 addition & 1 deletion config/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = get_wsgi_application()
application = get_wsgi_application()
Binary file modified requirements.txt
Binary file not shown.
1 change: 0 additions & 1 deletion services/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ def get_full_details(self):
return self.error_message



def custom_exception_handler(exc, context):
"""
DRF์˜ ์ปค์Šคํ…€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์„ค์ •ํ•˜๋ฉฐ, detail๋งŒ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฐ”๋˜ ๊ธฐ์กด ๋ฐฉ์‹์— ๋น„ํ•ด์„œ status code์™€ ๊ฐ™์€ ๋ถ€๊ฐ€ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ด ๋ณด๋ƒ…๋‹ˆ๋‹ค.
Expand Down
29 changes: 29 additions & 0 deletions services/public_data_portal_http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import requests

class HttpRequestException(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message


class PublicDataPortalHttpClient:
"""
ํ•ด๋‹น ํด๋ž˜์Šค๋Š” ๊ณต๊ณต ๋ฐ์ดํ„ฐ ํฌํƒˆ๊ณผ ์ง์ ‘ ์†Œํ†ตํ•˜๋Š” http ํด๋ผ์ด์–ธํŠธ ์ž…๋‹ˆ๋‹ค.
"""
def __init__(self, service_key):
# ๊ณต๊ณต ๋ฐ์ดํ„ฐ ํฌํƒˆ๊ณผ ์†Œํ†ตํ•˜๊ธฐ ์œ„ํ•œ ์„œ๋น„์Šค ํ‚ค์ž…๋‹ˆ๋‹ค.
self.service_key = service_key

def get_tour_api_response(self, path: str, **kwargs):
"""
ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ํ•œ๊ตญ๊ด€๊ด‘๊ณต์‚ฌ_๊ตญ๋ฌธ ๊ด€๊ด‘์ •๋ณด ์„œ๋น„์Šค_GW api์™€ ์ง์ ‘ ์†Œํ†ตํ•˜๋Š” ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค.
:param path: ์š”์ฒญ์„ ๋ณด๋‚ผ path๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
:param kwargs: ์„œ๋น„์Šค ํ‚ค๋ฅผ ์ œ์™ธํ•œ ์š”์ฒญ์„ ๋ณด๋‚ผ body๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ dictionary ํ˜•ํƒœ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.
"""
base_url = 'http://apis.data.go.kr/B551011/KorService2'
kwargs['serviceKey'] = self.service_key
response = requests.get(base_url + path, params=kwargs)
if response.status_code == 200:
return response.json()
raise HttpRequestException(f'Public Data Portal API HTTP request failed with status code {response.status_code}')
405 changes: 405 additions & 0 deletions services/tour_api_http_client.py

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions services/tour_api_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from .tour_api_http_client import *
from dataclasses import dataclass
from .exception_handler import *
from config.settings import PUBLIC_DATA_PORTAL_API_KEY

@dataclass
class Place:
"""
์žฅ์†Œ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค ์ž…๋‹ˆ๋‹ค.
"""
addr1: str = None # ์ƒ์„ธ์ฃผ์†Œ
addr2: str = None # ์ง€์—ญ์ฝ”๋“œ
areacode: str = None # ๋Œ€๋ถ„๋ฅ˜์ฝ”๋“œ
cat1: str = None # ์ค‘๋ถ„๋ฅ˜์ฝ”๋“œ
cat2: str = None # ์†Œ๋ถ„๋ฅ˜์ฝ”๋“œ
cat3: str = None # ์ƒ์„ธ์ฃผ์†Œ
contentid: str = None # ์ฝ˜ํ…์ธ ID
contenttypeid: str = None # ๊ด€๊ด‘ํƒ€์ž…(๊ด€๊ด‘์ง€, ์ˆ™๋ฐ•๋“ฑ) ID
created_time: str = None # ์ฝ˜ํ…์ธ ์ตœ์ดˆ๋“ฑ๋ก์ผ
firstimage: str = None # ์›๋ณธ๋Œ€ํ‘œ์ด๋ฏธ์ง€
cpyrhtDivCd: str = None # Type1:์ œ1์œ ํ˜•(์ถœ์ฒ˜ํ‘œ์‹œ-๊ถŒ์žฅ) Type3:์ œ3์œ ํ˜•(์ œ1์œ ํ˜• + ๋ณ€๊ฒฝ๊ธˆ์ง€)
mapx: str = None # GPS X์ขŒํ‘œ(WGS84 ๊ฒฝ๋„์ขŒํ‘œ) ์‘๋‹ต
mapy: str = None # GPS Y์ขŒํ‘œ(WGS84 ๊ฒฝ๋„์ขŒํ‘œ) ์‘๋‹ต
mlevel: str = None # Map Level ์‘๋‹ต
modifiedtime: str = None # ์ฝ˜ํ…์ธ ์ˆ˜์ •์ผ
sigungucode: str = None # ์‹œ๊ตฐ๊ตฌ์ฝ”๋“œ
tel: str = None # ์ „ํ™”๋ฒˆํ˜ธ
title: str = None # ์ฝ˜ํ…์ธ ์ œ๋ชฉ
zipcode: str = None # ์šฐํŽธ๋ฒˆํ˜ธ
lDongRegnCd: str = None # ๋ฒ•์ •๋™ ์‹œ๋„ ์ฝ”๋“œ
lDongSignguCd: str = None # ๋ฒ•์ •๋™ ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ
lclsSystm1: str = None # ๋ถ„๋ฅ˜์ฒด๊ณ„ ๋Œ€๋ถ„๋ฅ˜
lclsSystm2: str = None # ๋ถ„๋ฅ˜์ฒด๊ณ„ ์ค‘๋ถ„๋ฅ˜
lclsSystm3: str = None # ๋ถ„๋ฅ˜์ฒด๊ณ„ ์†Œ๋ถ„๋ฅ˜


class TourAPIService:
"""
ํ•ด๋‹น ํด๋ž˜์Šค๋Š” tour_api_http_client๋กœ ๋ฐ›์€ raw ๋ฐ์ดํ„ฐ ์ •๋ณด๋ฅผ ๊ฐ€๊ณตํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
"""
def __init__(self, service_key):
self.service_key = service_key
self.tour_api_http_client = TourAPIHTTPClient(service_key)

def get_area_based_list(self,
arrange: Arrange = None,
contentTypeId: ContentType = None,
area_info: Area = None,
category: Category = None,
modifiedtime: str = None,
ldong: lDong = None,
lclsSystem: lclsSystem = None
) -> list[Place]:
"""
ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ์žฅ์†Œ ์ •๋ณด๋ฅผ ๋ฆฌ์ŠคํŠธ ํ˜•์‹์œผ๋กœ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.
๊ฐ ์žฅ์†Œ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด Place ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.
"""
raw_data = self.tour_api_http_client.get_area_based_list(
arrange=arrange,
contentTypeId=contentTypeId,
area_info=area_info,
category=category,
modifiedtime=modifiedtime,
ldong=ldong,
lclsSystem=lclsSystem,
)
items = []
try:
items = raw_data['response']['body']['items']['item']
except KeyError:
raise HttpRequestException(
get_error_file(),
get_my_function(),
get_error_line(),
'tour api server exception',
f'raw data: {raw_data}'
)
# ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„˜์–ด์™”๋‹ค๊ณ  ๊ฐ€์ •.

places = []
for each in items:
# ๊ฐ each๋Š” ํŠน์ • ์žฅ์†Œ ์ •๋ณด๊ฐ€ ๋‹ด๊ธด dictionary ํ˜•์‹์ž…๋‹ˆ๋‹ค.
place = Place()
for key, value in each.items():
if hasattr(place, key):
setattr(place, key, value) # ์†์„ฑ ์ €์žฅ
places.append(place)
return places

def get_sigungu_code_as_name(self, area_code: str, target_sigungu_name: str):
"""
ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ์ „๊ตญ 17๊ฐœ ์‹œ/๋„ ์•ˆ์— ํฌํ•จ๋œ ํŠน์ • ์ง€์—ญ ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ๋ฅผ ์ด๋ฆ„์„ ํ†ตํ•ด์„œ ์–ป๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
ex) ๊ฐ•๋‚จ -> 1, ์•„์‚ฐ -> 12 (์˜ˆ์‹œ์ผ ๋ฟ์ด๋ฉฐ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ฐ’๊ณผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.)
"""
raw_data = self.tour_api_http_client.get_area_code(area_code=area_code)
items = raw_data['response']['body']['items']['item']
for item in items:
if target_sigungu_name in item['name']:
return item['code']
return None




if __name__ == '__main__':
tour_api_service = TourAPIService(PUBLIC_DATA_PORTAL_API_KEY)
print(tour_api_service.get_sigungu_code_as_name('1', '๊ฐ•๋‚จ'))
85 changes: 49 additions & 36 deletions tour/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,67 @@ async def connect(self):
##query_string##
- user_id: string (required)
- areaCode: string (required)
- sigunguName: string (required)
- contentTypeId: string (required)
- sigunguName: string (optional)
- categoryName: string (comma-separated, required)
- unique_code: string (optional)
"""
query_string = self.scope['query_string'].decode() # ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ๋ถˆ๋Ÿฌ๋“ค์ž…๋‹ˆ๋‹ค.
params = urllib.parse.parse_qs(query_string) # ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
self.user_id = params.pop('user_id', [None])[0] # user ๊ณ ์œ  sub๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
self.unique_code = params.pop('unique_code', [""])[0] # ์›น์†Œ์ผ“ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ณ ์œ  ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
days = params.pop('days', [None])[0] # ์—ฌํ–‰ ๊ธฐ๊ฐ„์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.
self.user_id = self.user_id + '_' + self.unique_code if self.unique_code != "" else self.user_id
query_string = self.scope['query_string'].decode()
params = urllib.parse.parse_qs(query_string)
self.user_id = params.pop('user_id', [None])[0]
self.unique_code = params.pop('unique_code', [""])[0]
self.user_id = self.user_id + '_' + self.unique_code if self.unique_code else self.user_id

if self.user_id is None:
await self.close()
return

# ์›น์†Œ์ผ“ ๊ทธ๋ฃน์— ๊ฐ€์ž…
# ๊ทธ๋ฃน ๊ฐ€์ž…
logger.info(f'channel_id: {self.user_id} ์›น์†Œ์ผ“ ๊ฐ€์ž…')
await self.channel_layer.group_add(self.user_id, self.channel_name) # user_id๋ฅผ ๊ทธ๋ฃน ์ด๋ฆ„์œผ๋กœ ํ•˜๊ณ  ์›น์†Œ์ผ“์— ๊ฐ€์ž…ํ•ฉ๋‹ˆ๋‹ค.
await self.accept() # ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ
await self.channel_layer.group_add(self.user_id, self.channel_name)
await self.accept()

# ์š”์ฒญ์„ celery task๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
areaCode = params.pop('areaCode', [None])[0] # area_code ๊ฐ€์ ธ์˜ด
sigunguName = params.pop('sigunguName', [None])[0] # ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„ ๊ฐ€์ ธ์˜ด
if areaCode is None or days is None: # areaCode๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด
# ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์‹ฑ
areaCode = params.pop('areaCode', [None])[0]
sigunguName = params.pop('sigunguName', [None])[0]
categoryName = params.pop('categoryName', [None])[0]

if areaCode is None or categoryName is None:
await self.send(text_data=json.dumps({
'state': 'ERROR',
'Message': 'ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ค‘ ์ผ๋ถ€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.'
}, ensure_ascii=False))
return

# ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ ํŒŒ์‹ฑ
tour = TourApi(MobileOS=MobileOS.ANDROID, MobileApp='AlphaProject2025', service_key=PUBLIC_DATA_PORTAL_API_KEY)
sigunguCode = None
sigunguCodes = None
if sigunguName is not None:
if sigunguName:
sigunguNames = sigunguName.split(',')
sigunguCodes = []
for each in sigunguNames:
sigunguCode = tour.get_sigungu_code(areaCode, each) # ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์— ๋Œ€์‘๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
if sigunguCode is None: # ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค๋ฉด
sigunguCode = tour.get_sigungu_code(areaCode, each)
if sigunguCode is None:
await self.send(text_data=json.dumps({
'state': 'ERROR',
'Message': 'ํ•ด๋‹น ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์— ๋Œ€์‘๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ™•์ธ ๋ฐ”๋ž๋‹ˆ๋‹ค.'
}, ensure_ascii=False))
return
sigunguCodes.append(sigunguCode)

task_result = app.send_task('tour.tasks.get_recommended_tour_based_area', args=[self.user_id, # ์ฑ„๋„ ๋ ˆ์ด์–ด ๊ทธ๋ฃน ํŠน์ •์„ ์œ„ํ•ด ๋ณด๋ƒ…๋‹ˆ๋‹ค.
areaCode, days, Arrange.TITLE_IMAGE.value, sigunguCodes])
# categoryName โ†’ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ ํ›„ task ํ˜ธ์ถœ
categoryNames = categoryName.split(',')
task_result = app.send_task(
'tour.tasks.get_recommended_place_by_category_task',
args=[self.user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value, self.user_id] # โ† ์ถ”๊ฐ€๋จ
)

await self.send(text_data=json.dumps({
'state': 'OK',
'Message': {
'task_id': task_result.task_id,
}
}))


async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.user_id, self.channel_name)

Expand All @@ -76,44 +83,50 @@ async def task_update(self, event):

async def receive(self, text_data=None, bytes_data=None):
"""
์žฌ์‹œ๋„๋ฅผ ์œ„ํ•œ ๋ฉ”์‹œ์ง€ ์ž…๋‹ˆ๋‹ค.
์žฌ์‹œ๋„๋ฅผ ์œ„ํ•œ ๋ฉ”์‹œ์ง€์ž…๋‹ˆ๋‹ค.
"""
data = json.loads(text_data)
user_id = data.get("user_id", None)
areaCode = data.get("areaCode", None)
sigunguName = data.get("sigunguName", None)
unique_code = data.get('unique_code', "") # ์›น์†Œ์ผ“ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ณ ์œ  ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
user_id = user_id + '_' + unique_code
days = data.get("days", None)
if user_id is None or areaCode is None or days is None:
# ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋‹ค๋ฉด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
unique_code = data.get("unique_code", "") # ์›น์†Œ์ผ“ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ณ ์œ  ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
categoryName = data.get("categoryName", None)
user_id = user_id + '_' + unique_code if unique_code else user_id

if user_id is None or areaCode is None or categoryName is None:
await self.send(text_data=json.dumps({
'state': 'ERROR',
'Message': 'ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ ์ค‘ ์ผ๋ถ€๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'
}, ensure_ascii=False))
return

# ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ ํŒŒ์‹ฑ
tour = TourApi(MobileOS=MobileOS.ANDROID, MobileApp='AlphaProject2025', service_key=PUBLIC_DATA_PORTAL_API_KEY)
sigunguCodes = None
if sigunguName is not None:
sigunguCodes = []
if sigunguName:
sigunguNames = sigunguName.split(',')
sigunguCodes = []
for each in sigunguNames:
sigunguCode = tour.get_sigungu_code(areaCode, each) # ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์— ๋Œ€์‘๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
if sigunguCode is None: # ์‹œ๊ตฐ๊ตฌ ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค๋ฉด
sigunguCode = tour.get_sigungu_code(areaCode, each)
if sigunguCode is None:
await self.send(text_data=json.dumps({
'state': 'ERROR',
'Message': 'ํ•ด๋‹น ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์— ๋Œ€์‘๋˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ๊ตฐ๊ตฌ ์ด๋ฆ„์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ™•์ธ ๋ฐ”๋ž๋‹ˆ๋‹ค.'
}, ensure_ascii=False))
return
sigunguCodes.append(sigunguCode)

task_result = app.send_task('tour.tasks.get_recommended_tour_based_area',
args=[self.user_id, # ์ฑ„๋„ ๋ ˆ์ด์–ด ๊ทธ๋ฃน ํŠน์ •์„ ์œ„ํ•ด ๋ณด๋ƒ…๋‹ˆ๋‹ค.
areaCode, Arrange.TITLE_IMAGE.value, sigunguCodes])
# ์นดํ…Œ๊ณ ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜
categoryNames = categoryName.split(',')

task_result = app.send_task(
'tour.tasks.get_recommended_place_by_category_task',
args=[user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value, user_id] # โ† ์ถ”๊ฐ€๋จ
)

await self.send(text_data=json.dumps({
'state': 'OK',
'Message': {
'task_id': task_result.task_id,
}
}))
}))
2 changes: 2 additions & 0 deletions tour/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ class UserTourImages(models.Model):
def __str__(self):
return f"{self.tour.tour_name} - {self.tour.tour_date}"

def __str__(self):
return f"{self.tour.tour_name} - {self.tour.tour_date}"