μ΄ λΌμ΄λΈλ¬λ¦¬λ API μλ΅ νμ€νλ₯Ό μν ν΄λμ€μ λ©μλλ₯Ό μ 곡ν©λλ€.
- μλ΅ λ°μ΄ν°λ₯Ό νμ€νλ response ν¬λ§·μΌλ‘ μμ± (λ°μ΄ν° μ 곡 μΈ‘λ©΄)
- 리μ€νΈκ° μλ μλ΅ λ°μ΄ν° νμ€ν ꡬμ±
- νμ΄μ§λ€μ΄μ νν 리μ€νΈ ꡬμ±
- λ보기 νν 리μ€νΈ ꡬμ±
- μλ΅ λ°μ΄ν°λ‘ νμ€νλ λ§€ν κ°μ²΄ μμ± (λ°μ΄ν° μλΉ μΈ‘λ©΄)
- νμ€νλ response ν¬λ§·μ λ°μ΄ν°λ‘ λ§€ν
- νμ€νλ response ν¬λ§·μ λ°μ΄ν°λ‘ λ§€ν (νμ΄μ§λ€μ΄μ 리μ€νΈ)
- νμ€νλ response ν¬λ§·μ λ°μ΄ν°λ‘ λ§€ν (λ보기 νν 리μ€νΈ)
νμ€ API μ€νμ λ€μ λ§ν¬μ μ μλμ΄ μμ΅λλ€.
ν΄λ· μμ§μ: https://ihunet.atlassian.net/wiki/spaces/KUDOS/pages/3783786517/API+specification+V1.2
μΌλ° μ¬μ©μ: https://velog.io/@jogakdal/standard-api-specification
pip install standard-api-responseμ΄ νλ‘μ νΈμ μ 체 μμ€ μ½λλ₯Ό λ€μ΄ λ°μΌμλ €λ©΄ μ μ₯μλ₯Ό ν΄λ‘ νκ³ νμν μ’ μμ±μ μ€μΉνμμμ€:
git clone https://github.com/jogakdal/standard-api-response.git
cd <repository-directory>
pip install -r requirements.txtνμ€ API μλ΅μ ꡬμ±νλ ν΄λμ€μ λλ€.
-
μμ±:
status(str): μλ΅ μ±κ³΅ μ¬λΆ, 'success' λλ 'error'λ‘ μ§μ λ©λλ€.version(str): API λ²μ .datetime(datetime): μλ΅ μκ°.duration(int): μ²λ¦¬ μκ° (λ°λ¦¬μ΄).payload(generic): μλ΅ λ°μ΄ν°.
-
λ©μλ:
build(payload=None, callback=None, status=None, version=None):- μλ΅ λ°μ΄ν°, μλ΅ μν, API λ²μ μ μ΄μ©νμ¬ νμ€ μλ΅ κ°μ²΄(StandardResponse)λ₯Ό μμ±ν©λλ€.
- payloadκ° NoneμΈ κ²½μ° callback ν¨μλ₯Ό μ΄μ©νμ¬ payloadλ₯Ό μμ±ν©λλ€.
- duration μλ κ³μ°μ νλ €λ©΄ callback ν¨μλ₯Ό μ΄μ©νμ¬ payloadλ₯Ό μμ±ν΄μΌ ν©λλ€.
- callback ν¨μλ payload, status, versionμ λ°νν΄μΌ ν©λλ€.
- callback ν¨μκ° λ°νν statusκ° Noneμ΄ μλλ©΄ StandardResponse κ°μ²΄μ status νλμ μ§μ λ©λλ€.
- callback ν¨μκ° λ°νν versionμ΄ Noneμ΄ μλλ©΄ StandardResponse κ°μ²΄μ version νλμ μ§μ λ©λλ€.
- μ΄λ νμ΄λ‘λ μμ± μ€ λ°μν μ μλ μ€λ₯ μ½λλ₯Ό StandardResponse κ°μ²΄μ λ°μνκΈ° μν¨μ λλ€.
-
μ¬μ© μ:
class SamplePayload(BaseModel):
value_1: str
value_2: int
@app.get('/item')
async def sample_item():
def __lambda():
payload = SamplePayload(value_1='sample', value_2=0)
return payload, None, None
return StandardResponse.build(callback=__lambda)μ€λ₯ νμ΄λ‘λμ errors 리μ€νΈμ μ€λ₯ μμ΄ν μ ꡬμ±ν©λλ€.
- μμ±:
code(str): μ€λ₯ μ½λ.message(str): μ€λ₯ λ©μμ§.
μ€λ₯ νμ΄λ‘λλ₯Ό λνλ λλ€.
- μμ±:
errors(List[ErrorPayloadItem]): μ€λ₯ 리μ€νΈ.appendix(Optional[dict]): μΆκ° μ 보.
- λ©μλ:
add_error(code: str, message: str):- μ€λ₯ μμ΄ν μ μΆκ°ν©λλ€.
build(code, message, appendix: Optional[dict] = None):- λ¨μΌ μ€λ₯ μμ΄ν μΌλ‘ μ€λ₯ νμ΄λ‘λ κ°μ²΄λ₯Ό μμ±ν©λλ€.
code: μ€λ₯ μ½λ.message: μ€λ₯ λ©μμ§.appendix: μΆκ° μ 보 (μ ν μ¬ν).
νμ΄μ§ ννμ 리μ€νΈ μλ΅μ μμ±ν λ μ¬μ©ν©λλ€.
Genericμ μ΄μ©νμ¬ λ¦¬μ€νΈ μμ΄ν
μ μ€μ νμ
μ μ§μ ν μ μμ΅λλ€.
- μμ±:
page(PageInfo): νμ΄μ§ μ 보.order(Optional[OrderInfo]): μ λ ¬ μ 보items: (Items[I]): μμ΄ν μ 보
- λ©μλ:
build(total_items: int, page_size: int, current_page: int, items, order_info: OrderInfo=None):PageableListκ°μ²΄λ₯Ό μμ±ν©λλ€.- μ΄ μμ΄ν μμ νμ΄μ§ λΉ μμ΄ν μλ₯Ό μ΄μ©νμ¬ νμ΄μ§ μ 보(PageInfo κ°μ²΄)λ₯Ό μμ±ν©λλ€.
- μ¬μ© μ:
class SampleItem(BaseModel):
key: str
value: int
class SamplePageListPayload(BaseModel):
value_1: str
value_2: int
pageable: PageableList[SampleItem]
class SampleService:
def __init__(self):
self.item_list = []
for i in range(100):
self.item_list.append(SampleItem(key=f'key_{i}', value=i))
def get_pageable_list(self, page: int, page_size: int):
# page == 0 μ΄λ©΄ λͺ¨λ λ°μ΄ν° λ°ν
if page <= 0:
page = 1
page_size = len(self.item_list)
page_list = PageableList[SampleItem].build(
items=self.item_list[(page - 1) * page_size : page * page_size],
total_items=len(self.item_list),
page_size=page_size,
current_page=page
)
payload = SamplePageListPayload(
value_1='page_list_sample',
value_2=0,
pageable=page_list.model_dump() # Pydanticμμ custom modelμ λν μ§λ ¬νλ₯Ό μνν λ dictλ₯Ό μ¬μ©νλ―λ‘ dictλ‘ λ³ν
)
return payload
@app.get('/page_list/{page}')
async def sample_page_list(
page: int = Path(description='νμ΄μ§ λ²νΈ, 0μΈ κ²½μ° λͺ¨λ λ°μ΄ν° λ°ν', ge=0),
page_size: int = Query(default=10, description='νμ΄μ§ λΉ μμ΄ν
μ', ge=1),
):
def __lambda():
payload = sample_service.get_pageable_list(page, page_size)
return payload, None, None
sample_service = SampleService()
return StandardResponse.build(callback=__lambda)μ¦λΆ νμμ 리μ€νΈ μλ΅μ μμ±ν λ μ¬μ©ν©λλ€.
Genericμ μ΄μ©νμ¬ λ¦¬μ€νΈ μμ΄ν
μ μ€νμ
μ μ§μ ν μ μμ΅λλ€.
- μμ±:
cursor(CursorInfo): 컀μ μ 보.order(Optional[OrderInfo]): μ λ ¬ μ 보items: (Items[I]): μμ΄ν μ 보
- μ¬μ© μ:
class SampleItem(BaseModel):
key: str
value: int
class SampleIncrementalListPayload(BaseModel):
value_1: str
value_2: int
incremental: IncrementalList[SampleItem]
class SampleService:
def __init__(self):
self.item_list = []
for i in range(100):
self.item_list.append(SampleItem(key=f'key_{i}', value=i))
def get_incremental_list(self, start_index: int, how_many: int):
item_count = len(self.item_list)
if start_index >= item_count:
return SampleIncrementalListPayload(
value_1='no more item',
value_2=0,
incremental=IncrementalList[SampleItem](
cursor=CursorInfo(field='sequence', start=start_index, end=None, expandable=False),
order=OrderInfo(sorted=True, by=[OrderBy(field='key', direction=OrderDirection.ASC)]).model_dump(),
items=Items[SampleItem](total=item_count, current=0, list=[]).model_dump()
).model_dump()
)
real_fetch_size = min(how_many, item_count - start_index)
order = OrderInfo(
sorted=True,
by=[
OrderBy(field='key', direction=OrderDirection.ASC),
OrderBy(field='value', direction=OrderDirection.ASC)
]
)
items = Items.build(item_count, self.item_list[start_index: start_index + real_fetch_size])
cursor = CursorInfo.build_from_total(
start_index=start_index,
how_many=how_many,
total_items=item_count,
field='sequence'
)
incremental = IncrementalList[SampleItem](
cursor=cursor,
order=order.model_dump(),
items=items.model_dump()
)
return SampleIncrementalListPayload(
value_1='expandable_list_sample',
value_2=0,
incremental=incremental.model_dump()
)
@app.get('/more_list/{start_index}')
async def sample_incremental_list(
start_index: int = Path(description='μμ μΈλ±μ€', ge=0),
how_many: int = Query(default=10, description='ν λ²μ κ°μ Έμ¬ μμ΄ν
μ', ge=1),
):
def __lambda():
payload = sample_service.get_incremental_list(start_index, how_many)
return payload, None, None
sample_service = SampleService()
return StandardResponse.build(callback=__lambda)νμ΄μ§ μ 보λ₯Ό ꡬμ±ν©λλ€. PageableListμ page μμ±μ ꡬμ±ν λ μ¬μ©ν©λλ€.
- μμ±:
size(int): νμ΄μ§ λΉ μμ΄ν μcurrent(int): νμ¬ νμ΄μ§ λ²νΈtotal(int): μ 체 νμ΄μ§ μ
- λ©μλ:
calc_total_pages(total_items: int, page_size: int):- μ 체 μμ΄ν μμ νμ΄μ§ λΉ μμ΄ν μλ₯Ό μ΄μ©νμ¬ μ 체 νμ΄μ§ μλ₯Ό κ³μ°ν©λλ€.
컀μ μ 보λ₯Ό ꡬμ±ν©λλ€. IncrementalListμ cursor μμ±μ ꡬμ±ν λ μ¬μ©ν©λλ€.
- μμ±:
field(Optional[str]): 컀μμ κΈ°μ€μ΄ λλ νλ λͺ .start(Any): μμ μΈλ±μ€ λλ ν€.end(Any): λ μΈλ±μ€ λλ ν€.expandable(Optional[bool]): λ€μ μμ΄ν μ‘΄μ¬ μ¬λΆ.
- λ©μλ:
build_from_total(start_index: int, how_many: int, total_items: int, field: str=None, convert_index=lambda field_name, index: index)- μ΄ μμ΄ν
μμ μμ μΈλ±μ€, 리ν΄ν μμ΄ν
μλ₯Ό μ¬μ©νμ¬
CursorInfoκ°μ²΄λ₯Ό μμ±ν©λλ€. start_indexλ μ€μ 컀μ κΈ°μ€ νλμ νμ κ³Ό κ΄κ³μμ΄ μ μν (리μ€νΈμ) μΈλ±μ€ μ 보λ₯Ό μ λ¬ν΄μΌ ν©λλ€.- 리ν΄ν 컀μμ μ€μ κ°μ΄ 리μ€νΈμ μΈλ±μ€ μ λ³΄κ° μλλΌλ©΄
convert_indexμ½λ°± ν¨μλ₯Ό μ΄μ©νμ¬ μ»€μ κΈ°μ€ νλμ κ²μΌλ‘ λ³νν΄ μ€ μ μμ΅λλ€.
- μ΄ μμ΄ν
μμ μμ μΈλ±μ€, 리ν΄ν μμ΄ν
μλ₯Ό μ¬μ©νμ¬
- μ¬μ© μ:
IncrementalListν΄λμ€μ μ¬μ© μλ₯Ό μ°Έμ‘°νμμμ€.
μμ΄ν
μ 보λ₯Ό ꡬμ±ν©λλ€.
PageableList, IncrementalListμ items μμ±μ ꡬμ±ν λ μ¬μ©ν©λλ€.
- μμ±:
total(Optional[int]): μ 체 μμ΄ν μ.current(Optional[int]): νμ¬ μμ΄ν μ.list(list): μμ΄ν 리μ€νΈ.
- λ©μλ:
build(total_items: int, items)Itemsκ°μ²΄λ₯Ό μμ±ν©λλ€.currentλitems리μ€νΈμ μ€μ sizeλ‘ μ§μ λ©λλ€.
- μ¬μ© μ:
IncrementalListν΄λμ€μ μ¬μ© μλ₯Ό μ°Έμ‘°νμμμ€.
μ λ ¬ μ 보λ₯Ό λνλ
λλ€.
PageableList, IncrementalListμ order μμ±μ ꡬμ±ν λ μ¬μ©ν©λλ€.
- μμ±:
sorted(bool): μ λ ¬ μ¬λΆ.by(List[OrderBy]): μ λ ¬λ νλ.
- μ¬μ© μ:
IncrementalListν΄λμ€μ μ¬μ© μλ₯Ό μ°Έμ‘°νμμμ€.
μ λ ¬λ νλ μ 보λ₯Ό λνλ
λλ€.
OrderInfoμ by μμ±μ ꡬμ±ν λ μ¬μ©ν©λλ€.
- μμ±:
field(str): μ λ ¬ν νλ λͺ .direction(OrderDirection): μ λ ¬ λ°©ν₯. ("ASC":OrderDirection.ASC, "DESC":OrderDirection.DESC)
- μ¬μ© μ:
IncrementalListν΄λμ€μ μ¬μ© μλ₯Ό μ°Έμ‘°νμμμ€.
μ λ ¬ λ°©ν₯μ μ§μ νλ Enum ν΄λμ€μ
λλ€.
OrderByμ direction μμ±μ μ§μ ν λ μ¬μ©ν©λλ€.
- μμ±:
ASC(str): μ€λ¦μ°¨μ.DESC(str): λ΄λ¦Όμ°¨μ.
- μ¬μ© μ:
IncrementalListν΄λμ€μ μ¬μ© μλ₯Ό μ°Έμ‘°νμμμ€.
νμ€ API μλ΅μ λμ κ°μ²΄λ‘ λ§€ννλ ν΄λμ€μ λλ€. κΈ°λ³Έμ μΌλ‘ ν΄λμ€ νλΌλ―Έν°λ‘ μλ΅ jsonμ μ§μ νλ©΄ ν΄λΉ jsonμ νμ΄μ¬ κ°μ²΄λ‘ λ³ννμ¬ response λ©€λ² λ³μμ μ μ₯ν©λλ€. κ°μ²΄λ₯Ό μμ±ν λ payload νμ μ μ§μ νλ©΄ response.payload λ©€λ² λ³μfmf ν΄λΉ νμ μΌλ‘ λͺ μν΄ μ€λλ€.
- λ©μλ:
__init__(response: dict, payload_type: Type[BaseModel]=None):- μλ΅ jsonκ³Ό payload νμ μ μ΄μ©νμ¬ κ°μ²΄λ₯Ό μμ±ν©λλ€.
map_payload(response: dict, payload_type: Type[P]) -> Type[P]:- μλ΅ jsonμ payloadλ₯Ό payload_typeμΌλ‘ λ§€νν©λλ€.
map_list(payload: dict, list_type: Type[P], list_key: str = 'pageable') -> Type[P]:- μ λ¬λ
payloadjsonμlist_keyν€μ ν΄λΉνλ 리μ€νΈλ₯Όlist_typeμΌλ‘ λ§€νν©λλ€.
- μ λ¬λ
map_pageable_list(payload: dict, item_type: Type[P], list_key: str = 'pageable') -> PageableList[P]:map_listν¨μμ PageableList λ²μ μ λλ€.
map_incremental_list(payload: dict, item_type: Type[P], list_key: str = 'incremental') -> IncrementalList[P]:map_listν¨μμ IncrementalList λ²μ μ λλ€.
- 'auto_map_list(payload: dict, item_type: Type[P]) -> Dict[str, _BaseList]'
payloadμ μλ list λ°μ΄ν°λ₯Ό μλμΌλ‘ λ³ννμ¬ λ°νν©λλ€.- νμ¬ PageableList, IncrementalList λ νμ λ§ μ§μν©λλ€.
payloadμ ν κ° μ΄μμ 리μ€νΈκ° μμ κ²½μ°, λͺ¨λ 리μ€νΈλ₯Ό λ³ννμ¬ {'<ν€νλ λͺ >: <κ°μ²΄>} ννλ‘ λ°νν©λλ€.
- μ¬μ© μ:
@pytest.mark.asyncio
async def test_page_list(start_api_server):
client = AsyncClient(base_url="http://localhost:5010")
response = await client.get(
url=f'/page_list/{1}',
params={
"page_size": 5
}
)
assert response.status_code == http.HTTPStatus.OK
json = response.json()
assert json['status'] == PayloadStatus.SUCCESS
mapper = StdResponseMapper(json, SamplePageListPayload)
assert mapper.response.status == PayloadStatus.SUCCESS
assert mapper.response.payload.pageable.page.size == 5
assert isinstance(mapper.response.payload, SamplePageListPayload)
assert isinstance(mapper.response.payload.pageable, PageableList)
assert isinstance(mapper.response.payload.pageable.items, Items)
assert isinstance(mapper.response.payload.pageable.items.list[0], SampleItem)
assert mapper.response.payload.pageable.page.current == 1
assert mapper.response.payload.pageable.items.current == 5
assert len(mapper.response.payload.pageable.items.list) == 5
assert mapper.response.payload.pageable.items.list[0].key == 'key_0'
assert mapper.response.payload.pageable.items.list[0].value == 0
payload = StdResponseMapper.map_payload(json, SamplePageListPayload)
assert isinstance(payload, SamplePageListPayload)
assert isinstance(payload.pageable, PageableList)
assert isinstance(payload.pageable.items, Items)
assert isinstance(payload.pageable.items.list[0], SampleItem)
# pageable = StdResponseMapper().map_list(json.get('payload'), PageableList[SampleItem], 'pageable')
pageable = StdResponseMapper.map_pageable_list(json.get('payload'), SampleItem, 'pageable')
assert isinstance(pageable, PageableList)
assert isinstance(pageable.items, Items)
assert isinstance(pageable.items.list[0], SampleItem)
assert pageable.page.size == 5
assert pageable.page.current == 1
assert pageable.items.current == 5
assert len(pageable.items.list) == 5
lists = StdResponseMapper.auto_map_list(json.get('payload'), SampleItem)
assert len(lists) == 1
assert isinstance(lists['pageable'], PageableList)
assert isinstance(lists['pageable'].items, Items)
assert isinstance(lists['pageable'].items.list[0], SampleItem)- ResponseKeyConverter ν΄λμ€λ₯Ό μ¬μ©νλ©΄ μλ΅μ μμ±ν λλ μλ΅μ λͺ¨λΈλ‘ λ§€νν λ νλλͺ μ΄λ νλλͺ μ μΌμ΄μ€ 컨벀μ μ λ³νν μ μμ΅λλ€.
- μμΈ μ€λͺ μ convertable-key-model λͺ¨λ μ€λͺ μλ₯Ό μ°Έμ‘°νμμμ€.
from pydantic import BaseModel
class SampleItem(BaseModel):
key: str
value: int
class SamplePageListPayload(ConvertableKeyModel):
value_1: str
value_2: int
pageable: PageableList[SampleItem]
class SampleService:
def __init__(self):
self.item_list = []
for i in range(100):
self.item_list.append(SampleItem(key=f'key_{i}', value=i))
def get_pageable_list(self, page: int, page_size: int):
# page == 0 μ΄λ©΄ λͺ¨λ λ°μ΄ν° λ°ν
if page <= 0:
page = 1
page_size = len(self.item_list)
page_list = PageableList[SampleItem].build(
items=self.item_list[(page - 1) * page_size: page * page_size],
total_items=len(self.item_list),
page_size=page_size,
current_page=page,
order_info=OrderInfo(sorted=True, by=[OrderBy(field='key', direction=OrderDirection.ASC)]),
)
payload = SamplePageListPayload(
value_1='page_list_sample',
value_2=0,
pageable=page_list.convert_key(), # Pydanticμμ custom modelμ λν μ§λ ¬νλ₯Ό μνν λ dictλ₯Ό μ¬μ©νλ―λ‘ dictλ‘ λ³ν
)
return payload
def test_with_standard_response_class():
def make_temporary_response():
def __lambda():
payload = sample_service.get_pageable_list(page=1, page_size=5)
return payload, None, None
sample_service = SampleService()
ResponseKeyConverter().clear()
ResponseKeyConverter().add_alias(StandardResponse, 'duration', 'duration_time')
ResponseKeyConverter().add_alias(PageInfo, 'current', 'current_page')
ResponseKeyConverter().add_alias(PageInfo, 'size', 'page_size')
ResponseKeyConverter().add_alias(PageInfo, 'total', 'total_pages')
ResponseKeyConverter().add_alias(OrderInfo, 'by', 'order_by')
ResponseKeyConverter().add_alias(Items[SampleItem], 'current', 'current_page')
ResponseKeyConverter().add_alias(PageableList[SampleItem], 'page', 'page_info')
ResponseKeyConverter().set_default_case_convention(CaseConvention.CAMEL)
result = StandardResponse.build(callback=__lambda)
result = result.convert_key()
ResponseKeyConverter().clear()
return result
response_json = make_temporary_response()
print(json.dumps(response_json, indent=2, ensure_ascii=False))
ResponseKeyConverter().add_alias(StandardResponse, 'duration', 'duration_time')
ResponseKeyConverter().add_alias(PageInfo, 'current', 'current_page')
ResponseKeyConverter().add_alias(PageInfo, 'size', 'page_size')
ResponseKeyConverter().add_alias(PageInfo, 'total', 'total_pages')
ResponseKeyConverter().add_alias(OrderInfo, 'by', 'order_by')
ResponseKeyConverter().add_alias(Items[SampleItem], 'current', 'current_page')
ResponseKeyConverter().add_alias(PageableList[SampleItem], 'page', 'page_info')
ResponseKeyConverter().set_default_case_convention(CaseConvention.CAMEL)
mapper = StdResponseMapper(response_json, SamplePageListPayload)
assert mapper.response.status == PayloadStatus.SUCCESS
assert mapper.response.payload.pageable.page.size == 5
assert isinstance(mapper.response.payload, SamplePageListPayload)
assert isinstance(mapper.response.payload.pageable, PageableList)
assert isinstance(mapper.response.payload.pageable.items, Items)
assert isinstance(mapper.response.payload.pageable.items.list[0], SampleItem)
assert mapper.response.payload.pageable.page.current == 1
assert mapper.response.payload.pageable.items.current == 5
assert len(mapper.response.payload.pageable.items.list) == 5
assert mapper.response.payload.pageable.items.list[0].key == 'key_0'
assert mapper.response.payload.pageable.items.list[0].value == 0
payload = StdResponseMapper.map_payload(response_json, SamplePageListPayload)
assert isinstance(payload, SamplePageListPayload)
assert isinstance(payload.pageable, PageableList)
assert isinstance(payload.pageable.items, Items)
assert isinstance(payload.pageable.items.list[0], SampleItem)
# pageable = StdResponseMapper().map_list(json.get('payload'), PageableList[SampleItem], 'pageable')
pageable = StdResponseMapper.map_pageable_list(response_json.get('payload'), SampleItem, 'pageable')
assert isinstance(pageable, PageableList)
assert isinstance(pageable.items, Items)
assert isinstance(pageable.items.list[0], SampleItem)
assert pageable.page.size == 5
assert pageable.page.current == 1
assert pageable.items.current == 5
assert len(pageable.items.list) == 5
lists = StdResponseMapper.auto_map_list(response_json.get('payload'), SampleItem)
assert len(lists) == 1
assert isinstance(lists['pageable'], PageableList)
assert isinstance(lists['pageable'].items, Items)
assert isinstance(lists['pageable'].items.list[0], SampleItem)
ResponseKeyConverter().clear()μ΄ λΌμ΄λΈλ¬λ¦¬λ λꡬλ μ¬μ©ν μ μλ ν리 μννΈμ¨μ΄μ λλ€. λ€λ§ μ½λλ₯Ό μμ ν κ²½μ° λ³κ²½λ λ΄μ©μ μμμ±μμκ² ν΅λ³΄ν΄ μ£Όμλ©΄ κ°μ¬νκ² μ΅λλ€.
ν©μ©νΈ(jogakdal@gmail.com)