From 68067acac100f3683876c455da835d0f78075b27 Mon Sep 17 00:00:00 2001 From: YimTaeKeun Date: Thu, 22 May 2025 13:46:25 +0900 Subject: [PATCH 01/15] =?UTF-8?q?FEAT:=20tour=20api2=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/public_data_portal_http_client.py | 29 ++ services/tour_api_service.py | 405 +++++++++++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 services/public_data_portal_http_client.py create mode 100644 services/tour_api_service.py diff --git a/services/public_data_portal_http_client.py b/services/public_data_portal_http_client.py new file mode 100644 index 0000000..f5cbc8a --- /dev/null +++ b/services/public_data_portal_http_client.py @@ -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}') diff --git a/services/tour_api_service.py b/services/tour_api_service.py new file mode 100644 index 0000000..de2ab10 --- /dev/null +++ b/services/tour_api_service.py @@ -0,0 +1,405 @@ +from types import FrameType +from typing import Literal + +from services.public_data_portal_http_client import PublicDataPortalHttpClient +from enum import Enum +from config.settings import PUBLIC_DATA_PORTAL_API_KEY +import inspect +from dataclasses import dataclass + +class Area: + """ + 지역에 관한 정보를 담고 있습니다. + Attributes: + areaCode: 지역코드(지역코드조회 참고) + sigunguCode: 시군구코드(지역코드조회 참고, areaCode 필수입력) + """ + def __init__(self, + areaCode: str = None, + sigunguCode: str = None + ): + self.areaCode = areaCode + self.sigunguCode = sigunguCode + self.__validate_parameters() + + def __validate_parameters(self): + if self.areaCode is None and self.sigunguCode is not None: # areaCode가 없는데 sigunguCode가 들어온 경우 + raise ValueError('지역 코드 없이 시군구 코드가 들어올 수 없습니다.') + +class Category: + """ + 카테고리에 관한 정보를 담고 있습니다. + Attributes: + cat1 (str): 대분류(서비스분류코드조회 참고) + cat2 (str): 중분류(서비스분류코드조회 참고, cat1 필수입력) + cat3 (str): 소분류(서비스분류코드조회 참고, cat1/cat2필수입력) + """ + + def __init__(self, + cat1: str = None, + cat2: str = None, + cat3: str = None): + self.cat1 = cat1 + self.cat2 = cat2 + self.cat3 = cat3 + self.__validate_parameters() + + def __validate_parameters(self): + if self.cat3 is not None: + if self.cat2 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.cat1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.cat2 is not None: + if self.cat1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + + +class lDong: + """ + 법정동 코드에 관한 클래스입니다. + Attributes: + lDongRegnCd (str): 법정동 시도 코드(법정동코드조회 참고) + lDongSigunguCd (str): 법정동 시군구 코드(법정동코드조회 참고, lDongRegnCd 필수입력) + """ + def __init__(self, + lDongRegnCd: str = None, + lDongSigunguCd: str = None): + self.lDongRegnCd = lDongRegnCd + self.lDongSigunguCd = lDongSigunguCd + self.__validate_parameters() + + def __validate_parameters(self): + if self.lDongSigunguCd is not None and self.lDongRegnCd is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + + + + +class lclsSystem: + """ + 분류체계에 관한 클래스 입니다. (분류체계 코드 조회 (get_lcls_system_code)참고) + Attributes: + lclsSystem1 (str): 분류체계 1Deth(분류체계코드조회 참고) + lclsSystem2 (str): 분류체계 2Deth(분류체계코드조회 참고, lclsSystm1 필수입력) + lclsSystem3 (str): 분류체계 3Deth(분류체계코드조회 참고, lclsSystm1/lclsSystm2 필수입력) + """ + def __init__(self, + lclsSystem1: str = None, + lclsSystem2: str = None, + lclsSystem3: str = None): + self.lclsSystem1 = lclsSystem1 + self.lclsSystem2 = lclsSystem2 + self.lclsSystem3 = lclsSystem3 + self.__validate_parameters() + + def __validate_parameters(self): + if self.lclsSystem3 is not None: + if self.lclsSystem2 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.lclsSystem1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.lclsSystem2 is not None: + if self.lclsSystem1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + +class ContentType(Enum): + """ + ContentType을 의미하는 이넘 클래스이며 해당 클래스가 가지고 있는 속석은 다음과 같습니다. + 관광지: GwanGwangJi + 문화사실: CultureInfra + 축제공연행사: FestivalAndConcert + 여행코스: TourCourse + 레포츠: LeisureSports + 숙박시설: Sukbak + 쇼핑: Shopping + 음식점: Restaurant + """ + GwanGwangJi = '12' + CultureInfra = '14' + FestivalAndConcert = '15' + TourCourse = '25' + LeisureSports = '28' + Sukbak = '32' + Shopping = '38' + Restaurant = '39' + +class Arrange(Enum): + """ + 절렬을 의미하는 이넘이며, 해당 클래스가 가지고 있는 속성은 아래와 같습니다. + Attribute: + Title: 제목순 + Modify: 수정일순 + Create: 생성일순 + ImageTitle: 이미지 있는 제목순 + ImageModify: 이미지 있는 수정일순 + ImageCreate: 이미지 있는 생성일순 + """ + Title = 'A' + Modify = 'C' # 수정일 순 + Create = 'D' # 생성일 순 + ImageTitle = 'O' # 이미지 반드시 있는 제목 순 + ImageModify = 'Q' # 이미지 반드시 있는 수정일 순 + ImageCreate = 'R' # 아마자 반드시 있는 생성일 순 + +class TourAPIService: + """ + 해당 서비스는 한국관광공사_국문 관광정보 서비스_GW에서 제공하는 관광정보를 얻기 위한 클래스입니다. + """ + def __init__(self, service_key: str, + mobile_os: Literal['AND', 'IOS', 'WEB', 'ETC'] ='AND', + mobile_app: str = 'conever_tour_api_service', + response_type: Literal['json', 'xml'] = 'json', + num_of_rows: int = 100,): + self.serviceKey = service_key # 서비스 키를 받습니다. + self.MobileOS = mobile_os # mobile os 값을 받습니다. + self.MobileApp = mobile_app # 앱 이름을 파라미터로 받습니다. + self._type = response_type # 기본 응답 데이터를 json 형태로 고정하여 받습니다. + self.numOfRows = num_of_rows # 한 페이지 결과 수를 의미하며 기본으로 한 번에 100개의 데이터를 받습니다. + self.required_params = self.__upload_required_params() # 필수 파라미터를 받은 직후 코드 배치 + self.http_client = PublicDataPortalHttpClient(service_key) # 하나의 통신 클라이언트 객체를 생성합니다. + + def __upload_required_params(self): + return self.__dict__.copy() + + + def get_area_code(self, area_code: str = None): + """ + 지역코드목록을 지역,시군구 코드목록을 조회하는 기능입니다. + :param area_code: 지역 코드를 의미하며, 해당 시/도 내의 시군구 코드 목록을 조회하기 위해서는 해당 시/도에 해당하는 지역 코드를 입력해주셔야 합니다. + """ + path = '/areaCode2' + params = self.required_params.copy() + if area_code is None: + return self.http_client.get_tour_api_response(path, **params) + params['areaCode'] = area_code + return self.http_client.get_tour_api_response(path, **params) + + + + def get_detail_pet_tour(self, content_id: str = None): + """ + 타입별 반려동물 동반 여행 정보를 조회하는 기능입니다. + :param content_id: 해당 장소(컨텐츠)별 고유 아이디를 말하며, 미 기입시 전체 목록을 조회합니다. + """ + path = '/detailPetTour2' + params = self.required_params.copy() + if content_id is None: + return self.http_client.get_tour_api_response(path, **params) + params['contentId'] = content_id + return self.http_client.get_tour_api_response(path, **params) + + def get_category_code(self, + contentTypeId: ContentType = None, + category: Category = None, + ): + """ + 서비스분류코드목록을 대,중,소분류로 조회하는 기능 + :param contentTypeId: ContentType Enum 클래스를 사용하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. + :param category: 카테고리를 의미하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. + """ + path = '/categoryCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + 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, + ): + """ + 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 + + Args: + arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 + contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 + area_info (Area): 지역 정보 (Area 클래스 주석 참고) + category (Category): 카테고리 정보 (카테고리 주석 참고) + modifiedtime (str): 수정일(형식 :YYYYMMDD) + ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) + lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) + + Returns: + The API response containing the filtered list of resources retrieved from the + Tour API. + + Raises: + Any error that might occur during the API request process. + """ + path = '/areaBasedList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_location_based_list(self, + mapX: str, + mapY: str, + radius: str, + arrange: Arrange = None, + contentTypeId: ContentType = None, + modifiedtime: str = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + area_info: Area = None, + category: Category = None, + ): + """ + 위치기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순,거리순 정렬검색목록을 조회하는 기능 + Args: + mapX (str): GPS X좌표(WGS84 경도좌표), required + mapY (str): GPS Y좌표(WGS84 경도좌표), required + radius (str): 거리반경(단위:m) , Max값 20000m=20Km, required + arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 + contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 + area_info (Area): 지역 정보 (Area 클래스 주석 참고) + category (Category): 카테고리 정보 (카테고리 주석 참고) + modifiedtime (str): 수정일(형식 :YYYYMMDD) + ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) + lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) + """ + path = '/locationBasedList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_keyword(self, + keyword: str, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None + ): + """ + 키워드로 검색을하며 전체별 타입정보별 목록을 조회한다 + """ + path = '/searchKeyword2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_festival(self, + eventStartDate: str, + eventEndDate: str = None, + arrange: Arrange = None, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None, + ): + path = '/searchFestival2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_sukbak(self, + lDongRegnCd: str, + lDongSigunguCd: str = None, + arrange: Arrange = None, + area_info: Area = None, + category: Category = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None): + """ + 숙박정보 검색목록을 조회한다. 컨텐츠 타입이 ‘숙박’일 경우에만 유효하다. + """ + path = '/searchStay2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_common(self): + # """ + # 타입별공통 정보기본정보,약도이미지,대표이미지,분류정보,지역정보,주소정보,좌표정보,개요정보,길안내정보,이미지정보,연계관광정보목록을 조회하는 기능 + # """ + # path = '/detailCommon2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_intro(self): + # """ + # 상세소개 쉬는날, 개장기간 등 내역을 조회하는 기능 + # """ + # path = '/detailIntro2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_info(self): + # """ + # 추가 관광정보 상세내역을 조회한다. 상세반복정보를 안내URL의 국문관광정보 상세 매뉴얼 문서를 참고하시기 바랍니다. + # """ + # path = '/detailInfo2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_image(self): + # """ + # 관광정보에 매핑되는 서브이미지목록 및 이미지 자작권 공공누리유형을 조회하는 기능 + # """ + # path = '/detailImage2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + def get_lcls_system_code(self, lclsSystem: lclsSystem = None, lclsSystemListYn: Literal['Y', 'N'] = 'Y'): + """ + 분류체계코드목록을 1Deth, 2Deth, 3Deth 코드별 조회하는 기능 + """ + path = '/lclsSystemCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_area_based_sync_list(self, + showflag: str = None, + arrange: Arrange = None, + contentTypeId: ContentType = None, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None, + oldContentId: str = None,): + """ + 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 + """ + path = '/areaBasedSyncList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_ldong_code(self, + lDongRegnCd: str = None, + lDongListYn: Literal['Y', 'N'] = 'Y', + ): + """ + 법정동코드 목록을 시도,시군구 코드별 조회하는 기능 + Args: + lDongRegnCd (str): 법정동 시도코드 ( lDongRegnCd 해당되는 법정동 시군구코드 조회 , 입력이 없을시 전체 시도목록 호출 ) + lDongListYn (str): 법정동 목록조회 여부(N:코드조회 , Y:전체목록조회) + """ + path = 'ldongCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def __upload_all_parameters(self, frame: FrameType): + """ + 모든 파라미터 업로드 진행 후 요청 보낼 최종 파라미터를 뽑아냅니다. + """ + arg_info = inspect.getargvalues(frame) + params = self.required_params.copy() + for arg in arg_info.args[1:]: # 1번 부터 시작 (self 제거) + if arg_info.locals[arg] is not None: + if type(arg_info.locals[arg]).__module__ != 'builtins' and not isinstance(arg_info.locals[arg], Enum): # 클래스 인스턴스라면 + for each in arg_info.locals[arg].__dict__: + if arg_info.locals[arg].__dict__[each] is not None: + params[each] = arg_info.locals[arg].__dict__[each] + else: + params[arg] = arg_info.locals[arg] if not isinstance(arg_info.locals[arg], Enum) else arg_info.locals[arg].value + return params + + + +if __name__ == '__main__': + tour_api_service = TourAPIService(PUBLIC_DATA_PORTAL_API_KEY) + print(tour_api_service.get_area_based_list(contentTypeId=ContentType.Sukbak, arrange=Arrange.ImageTitle)) + # area = Area() + # area.areaCode = 'sdf' + # print(area.__dict__) + # print(area.areaCode) \ No newline at end of file From 3cd82a50d5ed1fe13a6545d2069d2a94f7a4e604 Mon Sep 17 00:00:00 2001 From: YimTaeKeun Date: Thu, 22 May 2025 14:27:10 +0900 Subject: [PATCH 02/15] =?UTF-8?q?FIX:=20import=20=EC=88=98=EC=A0=95=20(#11?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/tour_api_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/tour_api_service.py b/services/tour_api_service.py index de2ab10..cfefce4 100644 --- a/services/tour_api_service.py +++ b/services/tour_api_service.py @@ -1,7 +1,7 @@ from types import FrameType from typing import Literal -from services.public_data_portal_http_client import PublicDataPortalHttpClient +from .public_data_portal_http_client import PublicDataPortalHttpClient from enum import Enum from config.settings import PUBLIC_DATA_PORTAL_API_KEY import inspect From 97d0c8a9e1d18b72d4658551197f793b3ad86f85 Mon Sep 17 00:00:00 2001 From: YimTaeKeun Date: Sat, 24 May 2025 23:02:44 +0900 Subject: [PATCH 03/15] =?UTF-8?q?FEAT:=20tour=20api=EC=9D=98=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20=EB=B6=80=EB=B6=84=EA=B3=BC=20=EA=B0=80=EA=B3=B5=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EB=B6=84=EB=A6=AC=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/exception_handler.py | 12 + services/tour_api_http_client.py | 405 +++++++++++++++++++++++++++ services/tour_api_service.py | 466 ++++++------------------------- 3 files changed, 501 insertions(+), 382 deletions(-) create mode 100644 services/tour_api_http_client.py diff --git a/services/exception_handler.py b/services/exception_handler.py index 14a9e74..8c4b905 100644 --- a/services/exception_handler.py +++ b/services/exception_handler.py @@ -124,6 +124,18 @@ def __init__(self, error_file, error_func, error_line, error_code=None, error_me error_message ) +class HttpRequestException(ExceptionHandler): + def __init__(self, error_file, error_func, error_line, error_code=None, error_message=None): + error_code = error_code or 'Http_Request_Error' + error_message = error_message or 'HTTP 서버 통신 오류.' + super().__init__( + error_file, + error_func, + error_line, + error_code, + error_message + ) + def custom_exception_handler(exc, context): diff --git a/services/tour_api_http_client.py b/services/tour_api_http_client.py new file mode 100644 index 0000000..4008ecd --- /dev/null +++ b/services/tour_api_http_client.py @@ -0,0 +1,405 @@ +from types import FrameType +from typing import Literal + +from .public_data_portal_http_client import PublicDataPortalHttpClient +from enum import Enum +from config.settings import PUBLIC_DATA_PORTAL_API_KEY +import inspect +from dataclasses import dataclass + +class Area: + """ + 지역에 관한 정보를 담고 있습니다. + Attributes: + areaCode: 지역코드(지역코드조회 참고) + sigunguCode: 시군구코드(지역코드조회 참고, areaCode 필수입력) + """ + def __init__(self, + areaCode: str = None, + sigunguCode: str = None + ): + self.areaCode = areaCode + self.sigunguCode = sigunguCode + self.__validate_parameters() + + def __validate_parameters(self): + if self.areaCode is None and self.sigunguCode is not None: # areaCode가 없는데 sigunguCode가 들어온 경우 + raise ValueError('지역 코드 없이 시군구 코드가 들어올 수 없습니다.') + +class Category: + """ + 카테고리에 관한 정보를 담고 있습니다. + Attributes: + cat1 (str): 대분류(서비스분류코드조회 참고) + cat2 (str): 중분류(서비스분류코드조회 참고, cat1 필수입력) + cat3 (str): 소분류(서비스분류코드조회 참고, cat1/cat2필수입력) + """ + + def __init__(self, + cat1: str = None, + cat2: str = None, + cat3: str = None): + self.cat1 = cat1 + self.cat2 = cat2 + self.cat3 = cat3 + self.__validate_parameters() + + def __validate_parameters(self): + if self.cat3 is not None: + if self.cat2 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.cat1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.cat2 is not None: + if self.cat1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + + +class lDong: + """ + 법정동 코드에 관한 클래스입니다. + Attributes: + lDongRegnCd (str): 법정동 시도 코드(법정동코드조회 참고) + lDongSigunguCd (str): 법정동 시군구 코드(법정동코드조회 참고, lDongRegnCd 필수입력) + """ + def __init__(self, + lDongRegnCd: str = None, + lDongSigunguCd: str = None): + self.lDongRegnCd = lDongRegnCd + self.lDongSigunguCd = lDongSigunguCd + self.__validate_parameters() + + def __validate_parameters(self): + if self.lDongSigunguCd is not None and self.lDongRegnCd is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + + + + +class lclsSystem: + """ + 분류체계에 관한 클래스 입니다. (분류체계 코드 조회 (get_lcls_system_code)참고) + Attributes: + lclsSystem1 (str): 분류체계 1Deth(분류체계코드조회 참고) + lclsSystem2 (str): 분류체계 2Deth(분류체계코드조회 참고, lclsSystm1 필수입력) + lclsSystem3 (str): 분류체계 3Deth(분류체계코드조회 참고, lclsSystm1/lclsSystm2 필수입력) + """ + def __init__(self, + lclsSystem1: str = None, + lclsSystem2: str = None, + lclsSystem3: str = None): + self.lclsSystem1 = lclsSystem1 + self.lclsSystem2 = lclsSystem2 + self.lclsSystem3 = lclsSystem3 + self.__validate_parameters() + + def __validate_parameters(self): + if self.lclsSystem3 is not None: + if self.lclsSystem2 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.lclsSystem1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + elif self.lclsSystem2 is not None: + if self.lclsSystem1 is None: + raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') + +class ContentType(Enum): + """ + ContentType을 의미하는 이넘 클래스이며 해당 클래스가 가지고 있는 속석은 다음과 같습니다. + 관광지: GwanGwangJi + 문화사실: CultureInfra + 축제공연행사: FestivalAndConcert + 여행코스: TourCourse + 레포츠: LeisureSports + 숙박시설: Sukbak + 쇼핑: Shopping + 음식점: Restaurant + """ + GwanGwangJi = '12' + CultureInfra = '14' + FestivalAndConcert = '15' + TourCourse = '25' + LeisureSports = '28' + Sukbak = '32' + Shopping = '38' + Restaurant = '39' + +class Arrange(Enum): + """ + 절렬을 의미하는 이넘이며, 해당 클래스가 가지고 있는 속성은 아래와 같습니다. + Attribute: + Title: 제목순 + Modify: 수정일순 + Create: 생성일순 + ImageTitle: 이미지 있는 제목순 + ImageModify: 이미지 있는 수정일순 + ImageCreate: 이미지 있는 생성일순 + """ + Title = 'A' + Modify = 'C' # 수정일 순 + Create = 'D' # 생성일 순 + ImageTitle = 'O' # 이미지 반드시 있는 제목 순 + ImageModify = 'Q' # 이미지 반드시 있는 수정일 순 + ImageCreate = 'R' # 아마자 반드시 있는 생성일 순 + +class TourAPIHTTPClient: + """ + 해당 서비스는 한국관광공사_국문 관광정보 서비스_GW에서 제공하는 관광정보를 얻기 위한 클래스로 한국관광 공사와 직접 소통하는 역할을 하는 클래스입니다. + """ + def __init__(self, service_key: str, + mobile_os: Literal['AND', 'IOS', 'WEB', 'ETC'] ='AND', + mobile_app: str = 'conever_tour_api_service', + response_type: Literal['json', 'xml'] = 'json', + num_of_rows: int = 100,): + self.serviceKey = service_key # 서비스 키를 받습니다. + self.MobileOS = mobile_os # mobile os 값을 받습니다. + self.MobileApp = mobile_app # 앱 이름을 파라미터로 받습니다. + self._type = response_type # 기본 응답 데이터를 json 형태로 고정하여 받습니다. + self.numOfRows = num_of_rows # 한 페이지 결과 수를 의미하며 기본으로 한 번에 100개의 데이터를 받습니다. + self.required_params = self.__upload_required_params() # 필수 파라미터를 받은 직후 코드 배치 + self.http_client = PublicDataPortalHttpClient(service_key) # 하나의 통신 클라이언트 객체를 생성합니다. + + def __upload_required_params(self): + return self.__dict__.copy() + + + def get_area_code(self, area_code: str = None): + """ + 지역코드목록을 지역,시군구 코드목록을 조회하는 기능입니다. + :param area_code: 지역 코드를 의미하며, 해당 시/도 내의 시군구 코드 목록을 조회하기 위해서는 해당 시/도에 해당하는 지역 코드를 입력해주셔야 합니다. + """ + path = '/areaCode2' + params = self.required_params.copy() + if area_code is None: + return self.http_client.get_tour_api_response(path, **params) + params['areaCode'] = area_code + return self.http_client.get_tour_api_response(path, **params) + + + + def get_detail_pet_tour(self, content_id: str = None): + """ + 타입별 반려동물 동반 여행 정보를 조회하는 기능입니다. + :param content_id: 해당 장소(컨텐츠)별 고유 아이디를 말하며, 미 기입시 전체 목록을 조회합니다. + """ + path = '/detailPetTour2' + params = self.required_params.copy() + if content_id is None: + return self.http_client.get_tour_api_response(path, **params) + params['contentId'] = content_id + return self.http_client.get_tour_api_response(path, **params) + + def get_category_code(self, + contentTypeId: ContentType = None, + category: Category = None, + ): + """ + 서비스분류코드목록을 대,중,소분류로 조회하는 기능 + :param contentTypeId: ContentType Enum 클래스를 사용하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. + :param category: 카테고리를 의미하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. + """ + path = '/categoryCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + 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, + ): + """ + 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 + + Args: + arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 + contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 + area_info (Area): 지역 정보 (Area 클래스 주석 참고) + category (Category): 카테고리 정보 (카테고리 주석 참고) + modifiedtime (str): 수정일(형식 :YYYYMMDD) + ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) + lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) + + Returns: + The API response containing the filtered list of resources retrieved from the + Tour API. + + Raises: + Any error that might occur during the API request process. + """ + path = '/areaBasedList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_location_based_list(self, + mapX: str, + mapY: str, + radius: str, + arrange: Arrange = None, + contentTypeId: ContentType = None, + modifiedtime: str = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + area_info: Area = None, + category: Category = None, + ): + """ + 위치기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순,거리순 정렬검색목록을 조회하는 기능 + Args: + mapX (str): GPS X좌표(WGS84 경도좌표), required + mapY (str): GPS Y좌표(WGS84 경도좌표), required + radius (str): 거리반경(단위:m) , Max값 20000m=20Km, required + arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 + contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 + area_info (Area): 지역 정보 (Area 클래스 주석 참고) + category (Category): 카테고리 정보 (카테고리 주석 참고) + modifiedtime (str): 수정일(형식 :YYYYMMDD) + ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) + lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) + """ + path = '/locationBasedList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_keyword(self, + keyword: str, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None + ): + """ + 키워드로 검색을하며 전체별 타입정보별 목록을 조회한다 + """ + path = '/searchKeyword2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_festival(self, + eventStartDate: str, + eventEndDate: str = None, + arrange: Arrange = None, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None, + ): + path = '/searchFestival2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_search_sukbak(self, + lDongRegnCd: str, + lDongSigunguCd: str = None, + arrange: Arrange = None, + area_info: Area = None, + category: Category = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None): + """ + 숙박정보 검색목록을 조회한다. 컨텐츠 타입이 ‘숙박’일 경우에만 유효하다. + """ + path = '/searchStay2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_common(self): + # """ + # 타입별공통 정보기본정보,약도이미지,대표이미지,분류정보,지역정보,주소정보,좌표정보,개요정보,길안내정보,이미지정보,연계관광정보목록을 조회하는 기능 + # """ + # path = '/detailCommon2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_intro(self): + # """ + # 상세소개 쉬는날, 개장기간 등 내역을 조회하는 기능 + # """ + # path = '/detailIntro2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_info(self): + # """ + # 추가 관광정보 상세내역을 조회한다. 상세반복정보를 안내URL의 국문관광정보 상세 매뉴얼 문서를 참고하시기 바랍니다. + # """ + # path = '/detailInfo2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + # def get_detail_image(self): + # """ + # 관광정보에 매핑되는 서브이미지목록 및 이미지 자작권 공공누리유형을 조회하는 기능 + # """ + # path = '/detailImage2' + # params = self.__upload_all_parameters(inspect.currentframe()) + # return self.http_client.get_tour_api_response(path, **params) + + def get_lcls_system_code(self, lclsSystem: lclsSystem = None, lclsSystemListYn: Literal['Y', 'N'] = 'Y'): + """ + 분류체계코드목록을 1Deth, 2Deth, 3Deth 코드별 조회하는 기능 + """ + path = '/lclsSystemCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_area_based_sync_list(self, + showflag: str = None, + arrange: Arrange = None, + contentTypeId: ContentType = None, + area_info: Area = None, + category: Category = None, + ldong: lDong = None, + lclsSystem: lclsSystem = None, + modifiedtime: str = None, + oldContentId: str = None,): + """ + 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 + """ + path = '/areaBasedSyncList2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def get_ldong_code(self, + lDongRegnCd: str = None, + lDongListYn: Literal['Y', 'N'] = 'Y', + ): + """ + 법정동코드 목록을 시도,시군구 코드별 조회하는 기능 + Args: + lDongRegnCd (str): 법정동 시도코드 ( lDongRegnCd 해당되는 법정동 시군구코드 조회 , 입력이 없을시 전체 시도목록 호출 ) + lDongListYn (str): 법정동 목록조회 여부(N:코드조회 , Y:전체목록조회) + """ + path = 'ldongCode2' + params = self.__upload_all_parameters(inspect.currentframe()) + return self.http_client.get_tour_api_response(path, **params) + + def __upload_all_parameters(self, frame: FrameType): + """ + 모든 파라미터 업로드 진행 후 요청 보낼 최종 파라미터를 뽑아냅니다. + """ + arg_info = inspect.getargvalues(frame) + params = self.required_params.copy() + for arg in arg_info.args[1:]: # 1번 부터 시작 (self 제거) + if arg_info.locals[arg] is not None: + if type(arg_info.locals[arg]).__module__ != 'builtins' and not isinstance(arg_info.locals[arg], Enum): # 클래스 인스턴스라면 + for each in arg_info.locals[arg].__dict__: + if arg_info.locals[arg].__dict__[each] is not None: + params[each] = arg_info.locals[arg].__dict__[each] + else: + params[arg] = arg_info.locals[arg] if not isinstance(arg_info.locals[arg], Enum) else arg_info.locals[arg].value + return params + + + +if __name__ == '__main__': + tour_api_service = TourAPIHTTPClient(PUBLIC_DATA_PORTAL_API_KEY) + print(tour_api_service.get_area_based_list(contentTypeId=ContentType.Sukbak, arrange=Arrange.ImageTitle)) + # area = Area() + # area.areaCode = 'sdf' + # print(area.__dict__) + # print(area.areaCode) \ No newline at end of file diff --git a/services/tour_api_service.py b/services/tour_api_service.py index cfefce4..b25982c 100644 --- a/services/tour_api_service.py +++ b/services/tour_api_service.py @@ -1,206 +1,46 @@ -from types import FrameType -from typing import Literal - -from .public_data_portal_http_client import PublicDataPortalHttpClient -from enum import Enum -from config.settings import PUBLIC_DATA_PORTAL_API_KEY -import inspect +from .tour_api_http_client import * from dataclasses import dataclass +from .exception_handler import * +from config.settings import PUBLIC_DATA_PORTAL_API_KEY -class Area: - """ - 지역에 관한 정보를 담고 있습니다. - Attributes: - areaCode: 지역코드(지역코드조회 참고) - sigunguCode: 시군구코드(지역코드조회 참고, areaCode 필수입력) - """ - def __init__(self, - areaCode: str = None, - sigunguCode: str = None - ): - self.areaCode = areaCode - self.sigunguCode = sigunguCode - self.__validate_parameters() - - def __validate_parameters(self): - if self.areaCode is None and self.sigunguCode is not None: # areaCode가 없는데 sigunguCode가 들어온 경우 - raise ValueError('지역 코드 없이 시군구 코드가 들어올 수 없습니다.') - -class Category: - """ - 카테고리에 관한 정보를 담고 있습니다. - Attributes: - cat1 (str): 대분류(서비스분류코드조회 참고) - cat2 (str): 중분류(서비스분류코드조회 참고, cat1 필수입력) - cat3 (str): 소분류(서비스분류코드조회 참고, cat1/cat2필수입력) - """ - - def __init__(self, - cat1: str = None, - cat2: str = None, - cat3: str = None): - self.cat1 = cat1 - self.cat2 = cat2 - self.cat3 = cat3 - self.__validate_parameters() - - def __validate_parameters(self): - if self.cat3 is not None: - if self.cat2 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - elif self.cat1 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - elif self.cat2 is not None: - if self.cat1 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - - -class lDong: - """ - 법정동 코드에 관한 클래스입니다. - Attributes: - lDongRegnCd (str): 법정동 시도 코드(법정동코드조회 참고) - lDongSigunguCd (str): 법정동 시군구 코드(법정동코드조회 참고, lDongRegnCd 필수입력) - """ - def __init__(self, - lDongRegnCd: str = None, - lDongSigunguCd: str = None): - self.lDongRegnCd = lDongRegnCd - self.lDongSigunguCd = lDongSigunguCd - self.__validate_parameters() - - def __validate_parameters(self): - if self.lDongSigunguCd is not None and self.lDongRegnCd is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - - - - -class lclsSystem: +@dataclass +class Place: """ - 분류체계에 관한 클래스 입니다. (분류체계 코드 조회 (get_lcls_system_code)참고) - Attributes: - lclsSystem1 (str): 분류체계 1Deth(분류체계코드조회 참고) - lclsSystem2 (str): 분류체계 2Deth(분류체계코드조회 참고, lclsSystm1 필수입력) - lclsSystem3 (str): 분류체계 3Deth(분류체계코드조회 참고, lclsSystm1/lclsSystm2 필수입력) + 장소 정보를 저장하는 데이터 클래스 입니다. """ - def __init__(self, - lclsSystem1: str = None, - lclsSystem2: str = None, - lclsSystem3: str = None): - self.lclsSystem1 = lclsSystem1 - self.lclsSystem2 = lclsSystem2 - self.lclsSystem3 = lclsSystem3 - self.__validate_parameters() + 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 # 분류체계 소분류 - def __validate_parameters(self): - if self.lclsSystem3 is not None: - if self.lclsSystem2 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - elif self.lclsSystem1 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - elif self.lclsSystem2 is not None: - if self.lclsSystem1 is None: - raise ValueError('파라미터 요건 불충족. 해당 클래스의 사용법을 읽어보신 후 재사용 부탁드립니다.') - -class ContentType(Enum): - """ - ContentType을 의미하는 이넘 클래스이며 해당 클래스가 가지고 있는 속석은 다음과 같습니다. - 관광지: GwanGwangJi - 문화사실: CultureInfra - 축제공연행사: FestivalAndConcert - 여행코스: TourCourse - 레포츠: LeisureSports - 숙박시설: Sukbak - 쇼핑: Shopping - 음식점: Restaurant - """ - GwanGwangJi = '12' - CultureInfra = '14' - FestivalAndConcert = '15' - TourCourse = '25' - LeisureSports = '28' - Sukbak = '32' - Shopping = '38' - Restaurant = '39' - -class Arrange(Enum): - """ - 절렬을 의미하는 이넘이며, 해당 클래스가 가지고 있는 속성은 아래와 같습니다. - Attribute: - Title: 제목순 - Modify: 수정일순 - Create: 생성일순 - ImageTitle: 이미지 있는 제목순 - ImageModify: 이미지 있는 수정일순 - ImageCreate: 이미지 있는 생성일순 - """ - Title = 'A' - Modify = 'C' # 수정일 순 - Create = 'D' # 생성일 순 - ImageTitle = 'O' # 이미지 반드시 있는 제목 순 - ImageModify = 'Q' # 이미지 반드시 있는 수정일 순 - ImageCreate = 'R' # 아마자 반드시 있는 생성일 순 class TourAPIService: """ - 해당 서비스는 한국관광공사_국문 관광정보 서비스_GW에서 제공하는 관광정보를 얻기 위한 클래스입니다. + 해당 클래스는 tour_api_http_client로 받은 raw 데이터 정보를 가공하여 데이터를 제공하는 역할을 합니다. """ - def __init__(self, service_key: str, - mobile_os: Literal['AND', 'IOS', 'WEB', 'ETC'] ='AND', - mobile_app: str = 'conever_tour_api_service', - response_type: Literal['json', 'xml'] = 'json', - num_of_rows: int = 100,): - self.serviceKey = service_key # 서비스 키를 받습니다. - self.MobileOS = mobile_os # mobile os 값을 받습니다. - self.MobileApp = mobile_app # 앱 이름을 파라미터로 받습니다. - self._type = response_type # 기본 응답 데이터를 json 형태로 고정하여 받습니다. - self.numOfRows = num_of_rows # 한 페이지 결과 수를 의미하며 기본으로 한 번에 100개의 데이터를 받습니다. - self.required_params = self.__upload_required_params() # 필수 파라미터를 받은 직후 코드 배치 - self.http_client = PublicDataPortalHttpClient(service_key) # 하나의 통신 클라이언트 객체를 생성합니다. - - def __upload_required_params(self): - return self.__dict__.copy() - - - def get_area_code(self, area_code: str = None): - """ - 지역코드목록을 지역,시군구 코드목록을 조회하는 기능입니다. - :param area_code: 지역 코드를 의미하며, 해당 시/도 내의 시군구 코드 목록을 조회하기 위해서는 해당 시/도에 해당하는 지역 코드를 입력해주셔야 합니다. - """ - path = '/areaCode2' - params = self.required_params.copy() - if area_code is None: - return self.http_client.get_tour_api_response(path, **params) - params['areaCode'] = area_code - return self.http_client.get_tour_api_response(path, **params) - - - - def get_detail_pet_tour(self, content_id: str = None): - """ - 타입별 반려동물 동반 여행 정보를 조회하는 기능입니다. - :param content_id: 해당 장소(컨텐츠)별 고유 아이디를 말하며, 미 기입시 전체 목록을 조회합니다. - """ - path = '/detailPetTour2' - params = self.required_params.copy() - if content_id is None: - return self.http_client.get_tour_api_response(path, **params) - params['contentId'] = content_id - return self.http_client.get_tour_api_response(path, **params) - - def get_category_code(self, - contentTypeId: ContentType = None, - category: Category = None, - ): - """ - 서비스분류코드목록을 대,중,소분류로 조회하는 기능 - :param contentTypeId: ContentType Enum 클래스를 사용하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. - :param category: 카테고리를 의미하며, 자세한 필드 속성은 해당 클래스 주석을 참고 바랍니다. - """ - path = '/categoryCode2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) + 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, @@ -209,197 +49,59 @@ def get_area_based_list(self, category: Category = None, modifiedtime: str = None, ldong: lDong = None, - lclsSystem: lclsSystem = None, - ): - """ - 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 - - Args: - arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 - contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 - area_info (Area): 지역 정보 (Area 클래스 주석 참고) - category (Category): 카테고리 정보 (카테고리 주석 참고) - modifiedtime (str): 수정일(형식 :YYYYMMDD) - ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) - lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) - - Returns: - The API response containing the filtered list of resources retrieved from the - Tour API. - - Raises: - Any error that might occur during the API request process. - """ - path = '/areaBasedList2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def get_location_based_list(self, - mapX: str, - mapY: str, - radius: str, - arrange: Arrange = None, - contentTypeId: ContentType = None, - modifiedtime: str = None, - ldong: lDong = None, - lclsSystem: lclsSystem = None, - area_info: Area = None, - category: Category = None, - ): - """ - 위치기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순,거리순 정렬검색목록을 조회하는 기능 - Args: - mapX (str): GPS X좌표(WGS84 경도좌표), required - mapY (str): GPS Y좌표(WGS84 경도좌표), required - radius (str): 거리반경(단위:m) , Max값 20000m=20Km, required - arrange (Arrange): 정렬 구분, 자세한 정렬 기준은 Arrange 이넘 클래스 참고 - contentTypeId (ContentType): 관광타입, 자세한 관광타입은 ContentType 이넘 클래스 참고 - area_info (Area): 지역 정보 (Area 클래스 주석 참고) - category (Category): 카테고리 정보 (카테고리 주석 참고) - modifiedtime (str): 수정일(형식 :YYYYMMDD) - ldong (lDong): 법정동 정보(lDong 클래스 주석 참고) - lclsSystem (lclsSystem): 법정 분류체계 (lclsySystem 주석 참고) - """ - path = '/locationBasedList2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def get_search_keyword(self, - keyword: str, - area_info: Area = None, - category: Category = None, - ldong: lDong = None, - lclsSystem: lclsSystem = None - ): - """ - 키워드로 검색을하며 전체별 타입정보별 목록을 조회한다 - """ - path = '/searchKeyword2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def get_search_festival(self, - eventStartDate: str, - eventEndDate: str = None, - arrange: Arrange = None, - area_info: Area = None, - category: Category = None, - ldong: lDong = None, - lclsSystem: lclsSystem = None, - modifiedtime: str = None, - ): - path = '/searchFestival2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) + 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 - def get_search_sukbak(self, - lDongRegnCd: str, - lDongSigunguCd: str = None, - arrange: Arrange = None, - area_info: Area = None, - category: Category = None, - lclsSystem: lclsSystem = None, - modifiedtime: str = None): - """ - 숙박정보 검색목록을 조회한다. 컨텐츠 타입이 ‘숙박’일 경우에만 유효하다. - """ - path = '/searchStay2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - # def get_detail_common(self): - # """ - # 타입별공통 정보기본정보,약도이미지,대표이미지,분류정보,지역정보,주소정보,좌표정보,개요정보,길안내정보,이미지정보,연계관광정보목록을 조회하는 기능 - # """ - # path = '/detailCommon2' - # params = self.__upload_all_parameters(inspect.currentframe()) - # return self.http_client.get_tour_api_response(path, **params) - - # def get_detail_intro(self): - # """ - # 상세소개 쉬는날, 개장기간 등 내역을 조회하는 기능 - # """ - # path = '/detailIntro2' - # params = self.__upload_all_parameters(inspect.currentframe()) - # return self.http_client.get_tour_api_response(path, **params) - - # def get_detail_info(self): - # """ - # 추가 관광정보 상세내역을 조회한다. 상세반복정보를 안내URL의 국문관광정보 상세 매뉴얼 문서를 참고하시기 바랍니다. - # """ - # path = '/detailInfo2' - # params = self.__upload_all_parameters(inspect.currentframe()) - # return self.http_client.get_tour_api_response(path, **params) - - # def get_detail_image(self): - # """ - # 관광정보에 매핑되는 서브이미지목록 및 이미지 자작권 공공누리유형을 조회하는 기능 - # """ - # path = '/detailImage2' - # params = self.__upload_all_parameters(inspect.currentframe()) - # return self.http_client.get_tour_api_response(path, **params) - - def get_lcls_system_code(self, lclsSystem: lclsSystem = None, lclsSystemListYn: Literal['Y', 'N'] = 'Y'): - """ - 분류체계코드목록을 1Deth, 2Deth, 3Deth 코드별 조회하는 기능 - """ - path = '/lclsSystemCode2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def get_area_based_sync_list(self, - showflag: str = None, - arrange: Arrange = None, - contentTypeId: ContentType = None, - area_info: Area = None, - category: Category = None, - ldong: lDong = None, - lclsSystem: lclsSystem = None, - modifiedtime: str = None, - oldContentId: str = None,): - """ - 지역기반 관광정보파라미터 타입에 따라서 제목순,수정일순,등록일순 정렬검색목록을 조회하는 기능 - """ - path = '/areaBasedSyncList2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def get_ldong_code(self, - lDongRegnCd: str = None, - lDongListYn: Literal['Y', 'N'] = 'Y', - ): - """ - 법정동코드 목록을 시도,시군구 코드별 조회하는 기능 - Args: - lDongRegnCd (str): 법정동 시도코드 ( lDongRegnCd 해당되는 법정동 시군구코드 조회 , 입력이 없을시 전체 시도목록 호출 ) - lDongListYn (str): 법정동 목록조회 여부(N:코드조회 , Y:전체목록조회) - """ - path = 'ldongCode2' - params = self.__upload_all_parameters(inspect.currentframe()) - return self.http_client.get_tour_api_response(path, **params) - - def __upload_all_parameters(self, frame: FrameType): - """ - 모든 파라미터 업로드 진행 후 요청 보낼 최종 파라미터를 뽑아냅니다. - """ - arg_info = inspect.getargvalues(frame) - params = self.required_params.copy() - for arg in arg_info.args[1:]: # 1번 부터 시작 (self 제거) - if arg_info.locals[arg] is not None: - if type(arg_info.locals[arg]).__module__ != 'builtins' and not isinstance(arg_info.locals[arg], Enum): # 클래스 인스턴스라면 - for each in arg_info.locals[arg].__dict__: - if arg_info.locals[arg].__dict__[each] is not None: - params[each] = arg_info.locals[arg].__dict__[each] - else: - params[arg] = arg_info.locals[arg] if not isinstance(arg_info.locals[arg], Enum) else arg_info.locals[arg].value - return params if __name__ == '__main__': tour_api_service = TourAPIService(PUBLIC_DATA_PORTAL_API_KEY) - print(tour_api_service.get_area_based_list(contentTypeId=ContentType.Sukbak, arrange=Arrange.ImageTitle)) - # area = Area() - # area.areaCode = 'sdf' - # print(area.__dict__) - # print(area.areaCode) \ No newline at end of file + print(tour_api_service.get_sigungu_code_as_name('1', '강남')) From 12f3f49b3fba7df4888fb221ab8d2c44856977c8 Mon Sep 17 00:00:00 2001 From: siyeon Date: Thu, 10 Jul 2025 14:14:14 +0900 Subject: [PATCH 04/15] =?UTF-8?q?modify=20:=20connect=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tour/consumers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tour/consumers.py b/tour/consumers.py index 90dc5ef..a53725b 100644 --- a/tour/consumers.py +++ b/tour/consumers.py @@ -35,6 +35,7 @@ async def connect(self): # 요청을 celery task로 보냅니다. areaCode = params.pop('areaCode', [None])[0] # area_code 가져옴 sigunguName = params.pop('sigunguName', [None])[0] # 시군구 이름 가져옴 + categoryName = params.pop('categoryName', [None])[0] # 카테고리 파라미터 가져옴 if areaCode is None or days is None: # areaCode가 존재하지 않는다면 await self.send(text_data=json.dumps({ 'state': 'ERROR', @@ -57,6 +58,18 @@ async def connect(self): return sigunguCodes.append(sigunguCode) + if categoryName: + task_result = app.send_task( + 'tour.tasks.get_recommended_place_by_category_task', + args=[self.user_id, areaCode, categoryName, sigunguCodes, Arrange.TITLE_IMAGE.value] + ) + else: + await self.send(text_data=json.dumps({ + 'state': 'ERROR', + 'Message': 'categoryName 파라미터가 누락되었습니다.' + }, ensure_ascii=False)) + return + task_result = app.send_task('tour.tasks.get_recommended_tour_based_area', args=[self.user_id, # 채널 레이어 그룹 특정을 위해 보냅니다. areaCode, days, Arrange.TITLE_IMAGE.value, sigunguCodes]) await self.send(text_data=json.dumps({ From 67df67e1ea27d93c2f2c45f116517fcbe857d95e Mon Sep 17 00:00:00 2001 From: siyeon Date: Thu, 10 Jul 2025 14:16:56 +0900 Subject: [PATCH 05/15] =?UTF-8?q?modify=20:=20receive=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tour/consumers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tour/consumers.py b/tour/consumers.py index a53725b..68df626 100644 --- a/tour/consumers.py +++ b/tour/consumers.py @@ -98,6 +98,7 @@ async def receive(self, text_data=None, bytes_data=None): unique_code = data.get('unique_code', "") # 웹소켓 통신을 위한 고유 번호를 가져옵니다. user_id = user_id + '_' + unique_code days = data.get("days", None) + categoryName = data.get("categoryName", None) if user_id is None or areaCode is None or days is None: # 데이터가 없다면 예외 처리 await self.send(text_data=json.dumps({ @@ -121,9 +122,10 @@ async def receive(self, text_data=None, bytes_data=None): 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]) + task_result = app.send_task( + 'tour.tasks.get_recommended_place_by_category_task', + args=[user_id, areaCode, categoryName, sigunguCodes, Arrange.TITLE_IMAGE.value] + ) await self.send(text_data=json.dumps({ 'state': 'OK', 'Message': { From 1fc6e708e611c2d9aacd67e0ebd381c076aa86b1 Mon Sep 17 00:00:00 2001 From: siyeon Date: Thu, 10 Jul 2025 14:57:15 +0900 Subject: [PATCH 06/15] =?UTF-8?q?connect=20=ED=95=A8=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tour/consumers.py | 62 +++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/tour/consumers.py b/tour/consumers.py index 68df626..1a7aaec 100644 --- a/tour/consumers.py +++ b/tour/consumers.py @@ -13,44 +13,46 @@ 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] # 시군구 이름 가져옴 - categoryName = params.pop('categoryName', [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': '해당 시군구 이름에 대응되는 코드를 가져올 수 없습니다. 시군구 이름을 다시 한번 확인 바랍니다.' @@ -58,20 +60,13 @@ async def connect(self): return sigunguCodes.append(sigunguCode) - if categoryName: - task_result = app.send_task( - 'tour.tasks.get_recommended_place_by_category_task', - args=[self.user_id, areaCode, categoryName, sigunguCodes, Arrange.TITLE_IMAGE.value] - ) - else: - await self.send(text_data=json.dumps({ - 'state': 'ERROR', - 'Message': 'categoryName 파라미터가 누락되었습니다.' - }, ensure_ascii=False)) - return + # 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] + ) - task_result = app.send_task('tour.tasks.get_recommended_tour_based_area', args=[self.user_id, # 채널 레이어 그룹 특정을 위해 보냅니다. - areaCode, days, Arrange.TITLE_IMAGE.value, sigunguCodes]) await self.send(text_data=json.dumps({ 'state': 'OK', 'Message': { @@ -79,7 +74,6 @@ async def connect(self): } })) - async def disconnect(self, close_code): await self.channel_layer.group_discard(self.user_id, self.channel_name) From c41ff0c0cebc34cf33d9d98b0d8f270949675bc5 Mon Sep 17 00:00:00 2001 From: siyeon Date: Thu, 10 Jul 2025 14:59:48 +0900 Subject: [PATCH 07/15] =?UTF-8?q?receive=20=ED=95=A8=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tour/consumers.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tour/consumers.py b/tour/consumers.py index 1a7aaec..27394d7 100644 --- a/tour/consumers.py +++ b/tour/consumers.py @@ -83,32 +83,32 @@ 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) + unique_code = data.get("unique_code", "") # 웹소켓 통신을 위한 고유 번호를 가져옵니다. categoryName = data.get("categoryName", None) - if user_id is None or areaCode is None or days is 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': '해당 시군구 이름에 대응되는 코드를 가져올 수 없습니다. 시군구 이름을 다시 한번 확인 바랍니다.' @@ -116,13 +116,17 @@ async def receive(self, text_data=None, bytes_data=None): return sigunguCodes.append(sigunguCode) + # 카테고리 파라미터를 리스트로 변환 + categoryNames = categoryName.split(',') + task_result = app.send_task( 'tour.tasks.get_recommended_place_by_category_task', - args=[user_id, areaCode, categoryName, sigunguCodes, Arrange.TITLE_IMAGE.value] + args=[user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value] ) + await self.send(text_data=json.dumps({ 'state': 'OK', 'Message': { 'task_id': task_result.task_id, } - })) \ No newline at end of file + })) From b3a0487fb25811fac879d0a5eff5313f77738540 Mon Sep 17 00:00:00 2001 From: siyeon Date: Mon, 14 Jul 2025 17:41:37 +0900 Subject: [PATCH 08/15] =?UTF-8?q?test=EB=A5=BC=20=EC=9C=84=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=ED=8C=8C=EC=9D=BC=EB=93=A4=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=96=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.py | 10 +++------- logs/app.log.2025-07-10 | 0 shared_db.sqlite3 | Bin 0 -> 217088 bytes tour/consumers.py | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) create mode 100644 logs/app.log.2025-07-10 create mode 100644 shared_db.sqlite3 diff --git a/config/settings.py b/config/settings.py index d03796e..4533fc2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -135,12 +135,8 @@ # 기본 데이터 베이스를 mysql로 설정합니다. DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': env('DB_NAME'), # DB 이름을 설정합니다. - 'USER': env('DB_USER'), # 접근 사용자 이름을 지정합니다. - 'PASSWORD': env('DB_PASSWORD'), # 접근 비밀번호를 지정합니다. - 'HOST': env('DB_HOST'), # mysql 접근 호스트를 의미합니다. - 'PORT': env('DB_PORT'), # 접근 포트 번호를 의미합니다. + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', } } @@ -309,7 +305,7 @@ }, 'loggers': { # 로거 설정, 실제 get_logger를 이용하여 로그 설정 가져옴 'django': { # 실제 배포 환경에서 사용하는 로거 - 'handlers': ['logstash'], + 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, diff --git a/logs/app.log.2025-07-10 b/logs/app.log.2025-07-10 new file mode 100644 index 0000000..e69de29 diff --git a/shared_db.sqlite3 b/shared_db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..ca7f0219327c2c39a67f8f44e24f1084fb26d4fc GIT binary patch literal 217088 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 literal 0 HcmV?d00001 diff --git a/tour/consumers.py b/tour/consumers.py index 27394d7..999c69f 100644 --- a/tour/consumers.py +++ b/tour/consumers.py @@ -64,7 +64,7 @@ async def connect(self): 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] + args=[self.user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value, self.user_id] # ← 추가됨 ) await self.send(text_data=json.dumps({ @@ -121,7 +121,7 @@ async def receive(self, text_data=None, bytes_data=None): task_result = app.send_task( 'tour.tasks.get_recommended_place_by_category_task', - args=[user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value] + args=[user_id, areaCode, categoryNames, sigunguCodes, Arrange.TITLE_IMAGE.value, user_id] # ← 추가됨 ) await self.send(text_data=json.dumps({ From 27cfdfcf003c225d9efabad42988ca681caa408a Mon Sep 17 00:00:00 2001 From: siyeon Date: Sat, 19 Jul 2025 23:10:45 +0900 Subject: [PATCH 09/15] =?UTF-8?q?modify=20=ED=83=9C=EA=B7=BC=EC=9D=B4?= =?UTF-8?q?=EA=B0=80=20=EA=B3=A0=EC=B9=98=EB=9D=BC=EA=B3=A0=20=ED=95=9C=20?= =?UTF-8?q?=EA=B2=83=EB=93=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/asgi.py | 2 +- config/settings.py | 30 ++++++++++++++++++++---------- config/wsgi.py | 2 +- requirements.txt | Bin 8128 -> 8130 bytes tour/models.py | 18 ++++++++++++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/config/asgi.py b/config/asgi.py index 80e6521..936e81c 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -27,4 +27,4 @@ application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), -}) +}) \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index 4533fc2..04f3e1d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -41,6 +41,7 @@ SKIP_TEST = env('SKIP_TEST') GEOCODER_API_KEY = env('GEOCODER_API_KEY') KAKAO_REAL_JAVASCRIPT_KEY = env('KAKAO_REAL_JAVASCRIPT_KEY') +REFRESH_TOKEN = env('REFRESH_TOKEN') # Quick-start development settings - unsuitable for production @@ -135,8 +136,12 @@ # 기본 데이터 베이스를 mysql로 설정합니다. DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.mysql', + 'NAME': env('DB_NAME'), # DB 이름을 설정합니다. + 'USER': env('DB_USER'), # 접근 사용자 이름을 지정합니다. + 'PASSWORD': env('DB_PASSWORD'), # 접근 비밀번호를 지정합니다. + 'HOST': env('DB_HOST'), # mysql 접근 호스트를 의미합니다. + 'PORT': env('DB_PORT'), # 접근 포트 번호를 의미합니다. } } @@ -231,8 +236,8 @@ # simple jwt setting SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), # 토큰 유효시간 설정 1시간으로 설정 - "REFRESH_TOKEN_LIFETIME": timedelta(days=5), # 리프레시 토큰 유효기간 설정 리프레시 토큰 유효기간은 5일로 설정 + "ACCESS_TOKEN_LIFETIME": timedelta(days=5), # 토큰 유효기간 5일로 설정 + "REFRESH_TOKEN_LIFETIME": timedelta(days=30), # 리프레시 토큰 유효기간 설정 리프레시 토큰 유효기간은 5일로 설정 "ROTATE_REFRESH_TOKENS": True, # 리프레시 토큰도 같이 반환됩니다. "BLACKLIST_AFTER_ROTATION": True, # 이전 토큰 블랙리스트 적용, 사용시 설치앱에 rest_framework_simplejwt.token_blacklist 추가 필요 "UPDATE_LAST_LOGIN": False, # last_login field가 업데이트 됩니다. (커스텀 모델이라 X) @@ -241,7 +246,7 @@ "SIGNING_KEY": SECRET_KEY, # 장고 자체의 시크릿 키로 signing key 지정 "VERIFYING_KEY": "", "AUDIENCE": None, - "ISSUER": None, # 토큰 발급자 명시 + "ISSUER": "Conever", # 토큰 발급자 명시 "JSON_ENCODER": None, "JWK_URL": None, "LEEWAY": 0, @@ -271,6 +276,9 @@ } # 아래는 로그 설정입니다. +LOG_DIR = './logs' +os.makedirs(LOG_DIR, exist_ok=True) + LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 기본 로거 설정 유지 @@ -280,7 +288,7 @@ 'style': '{', # str.format }, 'simple': { - 'format': '{name} {levelname} {asctime} {message}', + 'format': '[{levelname}] | {asctime} | {message}', 'style': '{', }, 'logstash': { @@ -290,10 +298,12 @@ 'handlers': { # 로그 핸들러 설정 'file': { 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': 'info.log', - 'formatter': 'verbose', - 'encoding': 'utf-8' + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': os.path.join(LOG_DIR, 'app.log'), + 'formatter': 'simple', + 'encoding': 'utf-8', + 'when': 'midnight', # 자정마다 새 로그파일 생성 + 'backupCount': 7, # 일주일치만 저장 }, 'logstash': { 'level': 'INFO', diff --git a/config/wsgi.py b/config/wsgi.py index e232e56..a6bccce 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -13,4 +13,4 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') -application = get_wsgi_application() +application = get_wsgi_application() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 76cd49a46e59fadbc5c3bdde36cba6e781490e7f..bfb3c0f5e12cce8be65a1c3de9bb60e21772088a 100644 GIT binary patch delta 10 RcmX?Lf5@Kc|Gy0f Date: Sun, 20 Jul 2025 00:20:29 +0900 Subject: [PATCH 10/15] log --- logs/app.log.2025-07-12 | 2 ++ logs/app.log.2025-07-19 | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 logs/app.log.2025-07-12 create mode 100644 logs/app.log.2025-07-19 diff --git a/logs/app.log.2025-07-12 b/logs/app.log.2025-07-12 new file mode 100644 index 0000000..14fb74f --- /dev/null +++ b/logs/app.log.2025-07-12 @@ -0,0 +1,2 @@ +[INFO] | 2025-07-12 22:26:43,790 | channel_id: test123_xyz 웹소켓 가입 +[INFO] | 2025-07-12 22:49:55,225 | channel_id: test123 웹소켓 가입 diff --git a/logs/app.log.2025-07-19 b/logs/app.log.2025-07-19 new file mode 100644 index 0000000..1b7e0b0 --- /dev/null +++ b/logs/app.log.2025-07-19 @@ -0,0 +1,5 @@ +[INFO] | 2025-07-19 23:22:05,130 | channel_id: 123_123 웹소켓 가입 +[INFO] | 2025-07-19 23:50:22,278 | channel_id: 123_123 웹소켓 가입 +[INFO] | 2025-07-19 23:51:50,931 | channel_id: 123_123 웹소켓 가입 +[INFO] | 2025-07-19 23:54:05,880 | channel_id: 123_123 웹소켓 가입 +[INFO] | 2025-07-19 23:55:28,278 | channel_id: 123_123 웹소켓 가입 From 77d20d64d8ecb9bcced0622e5826daac0b492559 Mon Sep 17 00:00:00 2001 From: YimTaeKeun <46028234+YimTaeKeun@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:00:29 +0900 Subject: [PATCH 11/15] Delete logs/app.log.2025-07-19 --- logs/app.log.2025-07-19 | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 logs/app.log.2025-07-19 diff --git a/logs/app.log.2025-07-19 b/logs/app.log.2025-07-19 deleted file mode 100644 index 1b7e0b0..0000000 --- a/logs/app.log.2025-07-19 +++ /dev/null @@ -1,5 +0,0 @@ -[INFO] | 2025-07-19 23:22:05,130 | channel_id: 123_123 웹소켓 가입 -[INFO] | 2025-07-19 23:50:22,278 | channel_id: 123_123 웹소켓 가입 -[INFO] | 2025-07-19 23:51:50,931 | channel_id: 123_123 웹소켓 가입 -[INFO] | 2025-07-19 23:54:05,880 | channel_id: 123_123 웹소켓 가입 -[INFO] | 2025-07-19 23:55:28,278 | channel_id: 123_123 웹소켓 가입 From 78b007832de4df6feff98fe7a83694be2c630534 Mon Sep 17 00:00:00 2001 From: YimTaeKeun <46028234+YimTaeKeun@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:00:44 +0900 Subject: [PATCH 12/15] Delete logs/app.log.2025-07-10 --- logs/app.log.2025-07-10 | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 logs/app.log.2025-07-10 diff --git a/logs/app.log.2025-07-10 b/logs/app.log.2025-07-10 deleted file mode 100644 index e69de29..0000000 From cf3cce5cd7b058eb89bc389f155f22c1a4ed3d3a Mon Sep 17 00:00:00 2001 From: YimTaeKeun <46028234+YimTaeKeun@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:00:57 +0900 Subject: [PATCH 13/15] Delete logs/app.log.2025-07-12 --- logs/app.log.2025-07-12 | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 logs/app.log.2025-07-12 diff --git a/logs/app.log.2025-07-12 b/logs/app.log.2025-07-12 deleted file mode 100644 index 14fb74f..0000000 --- a/logs/app.log.2025-07-12 +++ /dev/null @@ -1,2 +0,0 @@ -[INFO] | 2025-07-12 22:26:43,790 | channel_id: test123_xyz 웹소켓 가입 -[INFO] | 2025-07-12 22:49:55,225 | channel_id: test123 웹소켓 가입 From 09247af1ac9976d62ee251f528d4b3b16a0316d6 Mon Sep 17 00:00:00 2001 From: YimTaeKeun Date: Tue, 22 Jul 2025 23:25:29 +0900 Subject: [PATCH 14/15] =?UTF-8?q?CHORE:=20gitignore=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3bd587c..47d9c67 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ db.sqlite3 *.dir # 로그파일 무시 *.log +/logs +nginx.conf \ No newline at end of file From 335f6c56a6c51b6c8367a2c1b5523ae4c8777f49 Mon Sep 17 00:00:00 2001 From: YimTaeKeun Date: Tue, 22 Jul 2025 23:26:52 +0900 Subject: [PATCH 15/15] =?UTF-8?q?CHORE:=20git=20ignore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- requirements.txt | Bin 8130 -> 8132 bytes shared_db.sqlite3 | Bin 217088 -> 0 bytes 3 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 shared_db.sqlite3 diff --git a/.gitignore b/.gitignore index 47d9c67..913613b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ db.sqlite3 # 로그파일 무시 *.log /logs -nginx.conf \ No newline at end of file +nginx.conf +*.sqlite3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bfb3c0f5e12cce8be65a1c3de9bb60e21772088a..285e231fdf406edb5d2333849455572f3d9edc85 100644 GIT binary patch delta 13 UcmX?Pf5d(x(*b!FUIs1(03=)kvH$=8 delta 10 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