루나의 TIL 기술 블로그

위코드 두번째 프로젝트 험블벅 2 AWS S3 코드, 리팩토링

|

텀블벅 클론코딩

2차 프로젝트로 험블벅을 클론코딩하면서 프로젝트 이미지를 AWS S3서버에 올리는 코드를 작성했다.

https://github.com/boto/boto3/tree/develop/docs/source/guide

여기에 들어가보면 AWS공식 라이브러리인 boto3를 쓰는 예시가 자세히 잘 나와있다. 예를 들어서 Access permission을 받는 코드는 다음과 같다.

permission

AWS S3 권한 설정

권한 관리를 위해서 버킷 정책을 아래와같이 작성했다. 프로젝트 이미지라서 비회원한테도 보여야되니까 퍼블릭 엑세스 차단을 전부 해제했다.

** 권한 설정 비디오 튜토리얼 **

버킷 정책은 아래와 같이 했는데 버킷정책에 대한 자세한 내용은 AWS 사용설명서에 나와있는걸 읽어보면 된다. https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/example-policies-s3.html

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "StatementSid1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::182260599663:user/sang*****"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::humble/*"
        },
        {
            "Sid": "StatementSid2",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::humble/*"
        }
    ]
}
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "http://www.example1.com"
        ],
        "ExposeHeaders": [
            "ETag"
        ],
        "MaxAgeSeconds": 3000
    }
]

AWS S3액세스, ProjectView post

프로젝트를 업로드하는 뷰인데 이미지 파일 업로드가 함께 들어가있다

class ProjectView(View):
    @login_decorator
    def post(self, request):
        try:
            user = request.user
            data = request.POST

            image = request.FILES.get('image')
            #폼데이터로 받은 파일을 이미지 변수에 넣는다
            input_date = data.get('end_date')

            if not image:
                return JsonResponse({'MESSAGE' : 'IMAGE_EMPTY'}, status=400)

            upload_key = str(uuid.uuid4()) + image.name
            #uuid와 이미지 이름으로 키를 생성한다
            s3_client = boto3.client(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
            #s3서버에 접근한다

            Project.objects.create(
                name           = data.get('name'),
                user_id        = user.id,
                aim_amount     = data.get('aim_amount'),
                description    = data.get('description'),
                end_date       = input_date[6:10]+'-'+input_date[:2]+'-'+input_date[3:5],
                category_id    = data.get('category_id'),
                main_image_url = 'https://humblebug.s3.us-east-2.amazonaws.com/' + upload_key
                #aws버킷경로와 업로드 키를 포함한 URL을 저장한다. 이렇게 하고 퍼블릭 엑세스를 해제하면 url을 클릭했을 때 브라우저에서 바로 이미지가 나온다.  
            )

            s3_client.upload_fileobj(
                image,
                BUCKET_NAME,
                upload_key,
                ExtraArgs = {
                    'ContentType' : image.content_type #확장자였나..?
                }
            )
            #s3서버에 이미지를 업로드한다

            return JsonResponse({'MESSAGE':'SUCCESS'},status = 201)
    
        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404) 

이런 방식으로 쓰면 이미지 파일 수정 및 삭제할 때도 액세스 코드를 계속 써야되고 멘토님께서 나중에 키가 바뀌거나 아마존이 아닌 구글로 서비스업체를 바꾼다던지 했을 때 유지/보수 및 디버깅이 쉽게 하기 위해서 아래와 같이 리팩토링할 것을 조언해주셨다.

AWS S3 관련 코드 리팩토링

class CloudStorage:
    def __init__(self, ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME):
        self.ACCESS_KEY_ID = ACCESS_KEY_ID
        self.SECRET_ACCESS_KEY = SECRET_ACCESS_KEY
        self.BUCKET_NAME = BUCKET_NAME
        self.client = boto3.client(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
        self.resource = boto3.resource(
                's3',
                aws_access_key_id     = ACCESS_KEY_ID,
                aws_secret_access_key = SECRET_ACCESS_KEY,
            )
    #AWS 관련 데이터를 변수로 넣는다
        
    def upload_file(self, file):
        file_url = "사용하는 회사의 aws url.ap-northeast-2.rds.amazonaws.com/" + str(uuid.uuid1()) + file.name
        self.client.upload_fileobj(
                        file,
                        self.BUCKET_NAME,
                        file_url,
                        ExtraArgs={
                            "ContentType": file.content_type
                        }
        )
        return file_url

    def delete_file(self, file_url, project_id):
        main_image_url = Project.objects.get(id=project_id).file_url
        bucket = self.resource.Bucket(name=BUCKET_NAME)
        bucket.Object(key = file_url).delete()
    #AWS관련 액션들을 매쏘드로 넣는다

class ProjectUpload(View):
    @login_decorator
    def post(self, request):
        cloud_storage = CloudStorage(ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME)
        try:
            data = request.POST

            if request.FILES:
                file_url = (cloud_storage.upload_file(file))
                #그러면 이렇게 한 줄로 업로드하고 리턴 받은 url을 가져와서 사용할 수 있다.

            if not request.FILES:
                return JsonResponse({'MESSAGE' : 'IMAGE_EMPTY'}, status=400)

            Project.objects.create(
                    name           = data.get('name'),
                    user_id        = request.user.id,
                    aim_amount     = data.get('aim_amount'),
                    description    = data.get('description'),
                    end_date       = data.get('end_date')[0:4]+'-'+data.get('end_date')[5:7]+'-'+data.get('end_date')[8:10],
                    category_id    = data.get('category_id'),
                    main_image_url = file_url
                    #삭제하고 싶을 때는 앞에 url을 빼고 키값만 넣어줘야한다.
                    )
            return JsonResponse({'MESSAGE':'SUCCESS'}, status = 200)

        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404)

그 외 이미지 수정/삭제 코드

class ProjectModify(View):
    @login_decorator
    def post(self, request, project_id):
        try:
            data = request.POST

            if not Project.objects.filter(id=project_id, user_id=request.user.id).exists():
                return JsonResponse({'MESSAGE':'NOT_EXISTS'}, status=400)

            if request.FILES:
                cloud_storage.delete_file(file, file_url)
                file_url = (cloud_storage.upload_file(file))
                
            if not file:
                main_image_url = Project.objects.get(id=project_id).main_image_url

            Project.objects.filter(id=project_id, user_id=request.user.id).update(
                    name           = data.get('name'),
                    user_id        = request.user.id,
                    aim_amount     = data.get('aim_amount'),
                    description    = data.get('description'),
                    end_date       = data.get('end_date')[0:4]+'-'+data.get('end_date')[5:7]+'-'+data.get('end_date')[8:10],
                    category_id    = data.get('category_id'),
                    file_url = file_url
                    )
            return JsonResponse({'MESSAGE':'SUCCESS'}, status = 200)

        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404)

    @login_decorator
    def delete(self, request, project_id):
        cloud_storage = CloudStorage(ACCESS_KEY_ID, SECRET_ACCESS_KEY, BUCKET_NAME)
        user = request.user

        if not Project.objects.filter(id=project_id, user_id=user.id).exists():
            return JsonResponse({'MESSAGE':'NOT_EXISTS'}, status=400)
        
        file_url = Project.objects.get(id=project_id).file_url        
        cloud_storage.delete_file(file_url, project_id)

        Project.objects.get(id=project_id, user_id=user.id).delete()

        return JsonResponse({"MESSAGE": 'SUCCESS'}, status=204)

쉘을 통해서 AWS s3서버에서 이미지 삭제하는 코드

import boto3
ACCESS_KEY_ID = '**********'
SECRET_ACCESS_KEY = '******************'
BUCKET_NAME = '[name of your bucket]'
s3_client = boto3.client(
                 's3',
                 aws_access_key_id     = ACCESS_KEY_ID,
                 aws_secret_access_key = SECRET_ACCESS_KEY,
             )
s3_resource = boto3.resource(
                 's3',
                 aws_access_key_id     = ACCESS_KEY_ID,
                 aws_secret_access_key = SECRET_ACCESS_KEY,
             )
bucket = s3_resource.Bucket(name='버켓 이름')
#삭제하는 명령어1
bucket.Object('키값, 예를 들어 d457d1b0-169d-487a-950f-ef0cca576ad01.png').delete()
#삭제하는 명령어2
s3_client.delete_object(Bucket='humble', Key ='d457d1b0-169d-487a-950f-ef0cca576ad01.png')

위코드 두번째 프로젝트 험블벅 1 ERD, view 코드

|

텀블벅 클론코딩

ERD 모델

erd

  • 프로젝트 테이블

프로젝트에 필요한 정보를 모두 넣었다.

  • 후원(Patron) 테이블, 후원옵션(Option) 테이블

프로젝트마다 후원 옵션이 여러개 있고 유저가 후원을 하게 되면 후원테이블에 프로젝트 아이디, 옵션 아이디가 들어가도록 했다.
그런데 나중에 옵션 선택 후원 외에 금액을 추가해서 후원할 수 있는 기능을 추가구현해서 total_amount라는 컬럼을 추가해서 사용했다.

ProjectListView

  • 필터 + limit, offset + 쿼리셋 최적화 코드가 들어가있다.
class ProjectListView(View):
    def get(self, request):
        today = datetime.now()
        category_id = request.GET.get("categoryId")

        q = Q()

        if category_id:
            q &= Q(category_id = category_id)
        
        limit        = int(request.GET.get('limit', 12))
        #받는 limit값이 없으면 디폴트로 12개까지 부터 보여준다
        offset       = int(request.GET.get('offset', 0))
        #받는 offset값이 없으면 디폴트로 0개 부터 보여준다

        #하이라이트
        projects = Project.objects.annotate(total=Sum('patron__total_amount'), count=Count('patron')).prefetch_related('tag').select_related('user', 'category').filter(q).order_by('created_at')[offset:offset+limit]
        #패트론 테이블에서 레스토랑 아이디별로 total_amount컬럼 값을 합쳐서 저장하고 가상의 컬럼 patron__total_amount에 저장한다. 
        #정참조하는 user, category테이블의 내용을 골라서 가져오고 역참조하는 tag테이블의 내용을 미리 가져와서
        #q값(카테고리 아이디)으로 필터하고 예전에 만들어진 것부터 0부터12개 객체를 projects에 저장한다.  

        projects = [{
                'tag'             : [{'id':tag.id, 'name':tag.tag} for tag in project.tag.all()],
                'name'            : project.name,
                'id'              : project.id,
                'user_id'         : project.user_id,
                'user'            : project.user.nickname,
                'remaining_time'  : (project.end_date.replace(tzinfo=None) - today).days,
                #
                'main_image_url'  : project.main_image_url,
                'category_name'   : project.category.name,
                "aim_amount"      : project.aim_amount,
                'payment_date'    : project.end_date.strftime("%Y년 %m월 %d일"),
                #
                'collected_amount': project.total,
                'percentage' : '%.f%%'%(project.total/project.aim_amount*100) if project.total else '0%',
                #
                "created_at"      : project.created_at,
                "end_date"        : project.end_date, 
                "description"     : project.description
            } for project in projects]
            
        return JsonResponse({"project" : projects}, status=200)

코드카타 5주차 선택정렬, 버블정렬, 재귀, 연결리스트

|

DAY1

Selection Sort(선택정렬)

정렬 알고리즘은 순서가 없던 데이터를 순서대로 바꾸어 나열하는 알고리즘입니다. 정렬을 하는 방법은 여러가지가 있는데, 그 중에 유명한 알고리즘은 아래 4가지가 있습니다.

  • 선택정렬
  • 버블정렬
  • 삽입정렬
  • 퀵정렬

오늘은 선택정렬을 배우겠습니다. 선택정렬은 정렬되지 않은 데이터 중 가장 작은 데이터를 선택해서 맨 앞에서부터 순서대로 정렬해 나가는 알고리즘입니다.

영어 설명 한 번 보시죠!

It has O(n2) time complexity, making it inefficient on large lists, and generally performs worse than the similar insertion sort.
Selection sort is noted for its simplicity, and it has performance advantages over more complicated algorithms in certain situations, particularly where auxiliary memory is limited.

예제를 통해 보겠습니다.

정렬을 해야하는 배열은 [7,5,4,2] 입니다.

첫 번째 loop에서는 index 0부터 3까지 확인하며 가장 작은 수를 찾습니다. 2 이므로 index 0의 7과 교체합니다. -> [2,5,4,7]

두 번째는 index 1부터 3까지 확인하며 가장 작은 수를 찾습니다. 4이므로 index 1의 5와 교체합니다 -> [2,4,5,7]

세 번째는 index 2부터 3까지.. 이런식으로 가장 작은 수를 선택해서 순서대로 교체하는 것을 선택정렬이라고 합니다.

Problem Statement

nums라는 정렬되지 않은 숫자 배열을 주면, 오름차순(1,2,3..10) 으로 정렬된 배열을 return해주세요. 선택정렬 알고리즘으로 구현하셔야겠죠??

제출코드

def selectionSort(nums):
  for i in range(0,len(nums)):
      temp  = min(nums[i:])
      nums[nums.index(min(nums[i:]))] = nums[i]
      nums[i] = temp
  return nums

DAY2

버블정렬(Bubble Sort)

버블 정렬은 인접한 데이터를 교환해서 정렬하는 알고리즘입니다. 알고리즘의 정렬되는 모습이 마치 거품처럼 보인다고 해서 붙여진 이름입니다.아래 그림을 한 번 봐주세요. 아마 바로 이해되실 것입니다.

아래와 같은 정렬되지 않은 수가 있을 때, index 0 <-> 1 부터 교환하기 시작합니다. 인접한 두 수를 비교하여 더 큰 것을 우측으로 이동시킵니다. 6 5 3 2 8 -> 5 6 3 2 8

그 다음은 index 1 <-> 2 5 6 3 2 8 -> 5 3 6 2 8

그 다음은 index 2 <-> 3 5 3 6 2 8 -> 5 3 2 6 8

그 다음은 index 3 <-> 4 5 3 2 6 8 -> 5 3 2 6 8 이렇게 제일 마지막 두 수 까지 비교하면, 제일 큰 수가 제일 마지막 index에 위치하는 것을 알 수 있습니다.

다시 처음부터 시작합니다. 5 3 2 6 8 -> 3 5 2 6 8

3 5 2 6 8 -> 3 2 5 6 8

3 2 5 6 8 -> 3 2 5 6 8 이번 교환에는 index 2까지 비교하고 멈추면 됩니다. 마지막 index는 이미 제일 큰 수가 정렬된 상태이기 때문입니다. 이런식으로 계속 비교하고 교체하면 됩니다.!

Problem

nums라는 배열을 주면, 버블정렬 알고리즘으로 배열을 정렬해주세요.

사고 과정

def bubbleSort(arr):
  for i in range(0, len(arr)-1):
    if arr[i] > arr[i + 1]: 
      arr[i], arr[i + 1] = arr[i + 1], arr[i]
  return(arr)

이렇게하니 2번 테스트에서 에러가 났다.

모범 답안

def bubbleSort(arr):
  n = len(arr)
  for i in range(n):
    for j in range(0, n - i - 1):
      if arr[j] > arr[j + 1]: 
        arr[j], arr[j + 1] = arr[j + 1], arr[j]
  return(arr)

DAY3

재귀(Recursion)

이전에 재귀를 배웠었습니다. 오늘은 재귀를 이용해 문제를 풀어주세요.

str 이라는 ‘string’을 넘겨주면 글자순서를 바꿔서 return해주세요. reverse 메서드 사용은 당연히 금지입니다!

input: 'hello'
output: 'olleh'

힌트

아래의 코드가 어색한 것은 아니겠죠? (함수의 return에 string을 붙여서 사용하는 것)

def getName(name):
  return name

print(getName('김')+'님')

사고과정

def reverseString(str):
  print(str[:-1])
  return reverseString(str[:-1])

모범답안

def reverseString(str):
    if len(str)==1:
        return str[0]
    return str[-1] + reverseString(str[:-1])

DAY4

연결리스트(Linked List)

아래 설명을 먼저 읽어주세요. linked list에 대한 설명입니다.

Singly Linked List singly linked list에 대해 조금 더 자세히 알아보겠습니다. 위의 stack overflow 설명과 같이, singly-linked list는 특정 index의 node value를 바로 얻을 수가 없습니다.

만약 i번째 요소를 얻고 싶으면 head node부터 node를 하나하나 탐색해야 알 수 있습니다. 예를 들어 head의 node가 23이라고 할 때, 3번째 node를 알고 싶으면 head의 next 값을 알아서 -> 2번째 node를 알아서 -> 2번째 node를 방문해야만 3번째 node를 알 수 있습니다.

2번째 node가 6이라고 한다면 head node 23의 next는 6이었을 것이고, 다시 6을 방문해 next 값을 얻는 식으로 하나하나 거치며 도달할 수 있습니다.

이렇게 array에 비교해 비효율적이어 보이는 linked list를 왜 쓸까 궁금해 할 수 있겠지만,insert, delete의 기능을 추가한 linked list를 직접 설계할 수 있다면 마치 array를 사용하는 것처럼 편할 것입니다.

코드 카타

linked list를 만들 수 있는 MyLinkedList 클래스를 설계해봅시다. singly linked list 로 해도 되고, doubly linked list 로 구현하셔도 됩니다.

singly linked list를 구현하려면 val과 next라는 속성이 있어야 합니다. val: 현재 node의 value next: 다음 node를 가르키는 pointer(reference)

doubly linked list를 구현하려면 prev라는 속성이 하나 더 있어야 합니다. prev: 이전 node를 가르키는 pointer(reference)

MyLinkedList 클래스에는 5가지 method가 있습니다. 아래의 설명을 참고하여 구현해주세요.

  • get(index) : linked list 의 index 번째 node의 value를 return해주세요. 값이 없으면 -1을 return해주세요.

  • addAtHead(val) : linked list 의 첫 번째 요소 전에 value가 val인 node를 추가해주세요. val이 추가되면 이 node는 linked list의 첫 번째 노드가 되는 것입니다.

  • addAtTail(val) : value가 val인 node를 linked list의 마지막에 추가해주세요.

  • addAtIndex(index, val) : value가 val인 node를 linked list의 index node 바로 전에 추가해주세요. 만약 index가 linked list의 길이와 같다면 제일 마지막에 추가하면 됩니다. 만약 index가 길이보다 길다면, node를 추가하지 말아주세요.

  • deleteAtIndex(index) : linked list의 index 번째 node를 삭제해주세요.

MyLinkedList 클래스는 아래와 같이 사용됩니다.

linkedList = MyLinkedList()
linkedList.addAtHead(1)
linkedList.addAtTail(3)
linkedList.addAtIndex(1, 2)  // linked list는 1->2->3 가 됩니다.
linkedList.get(1)            // returns 2
linkedList.deleteAtIndex(1)  // linked list는 이제 1->3
linkedList.get(1)            // returns 3

모범답안

class Node:
  def __init__(self, value):
    self.val = value
    self.next = self.pre = None
        
  def getNext(self):
    return self.next
  

class MyLinkedList():
  
  def __init__(self):
    self.head = None
  
      
  def get(self, index):
    current = self.head
    while index > 0 :
      current = current.getNext()
      index -= 1
    return current.val
  
  
  def addAtHead(self, val):
    newNode = Node(val)
    newNode.next = self.head
    self.head = newNode
      
      
  def addAtTail(self, val):
    newNode = Node(val)
    current = self.head
    while current.next != None :
      current = current.getNext()
    
    current.next = newNode
      
  
  def addAtIndex(self, index, val):
    newNode = Node(val)
    current = self.head
    while index > 1 :
      current = current.getNext()
      index -= 1

    newNode.next = current.getNext()
    current.next = newNode
    
  
  def deleteAtIndex(self, index):
    current = self.head
    while index > 1 :
      current = current.getNext()
      index -= 1
    
    current.next = current.getNext().getNext()

코드카타 4주차

|

DAY1

문제

양수 N을 이진법으로 바꿨을 때, 연속으로 이어지는 0의 갯수가 가장 큰 값을 return해 주세요.

  • 이어지는 0은 1과 1사이에 있는 것을 의미합니다.
  • 이런 것을 binary gap 이라고 합니다.

예를 들어,

input: 9
output: 2
설명: 9의 이진수는 1001 입니다. 
1과 1사이에 있는 0은 2 이므로, 2를 return
input: 529
output: 4
설명: 529의 이진수는 1000010001 입니다. 
1과 1사이에 있는 연속된 0의 수는 4와 3입니다.
이 중 큰 값은 4이므로 4를 return
input: 20
output: 1
설명: 20의 이진수는 10100 입니다. 
1과 1사이에 있는 연속된 0의 수는 1 뿐입니다.
(뒤에 있는 0은 1사이에 있는 것이 아니므로)
input: 15
output: 0
설명: 15의 이진수는 1111 입니다. 
1과 1사이에 있는 0이 없으므로 0을 return
input: 32
output: 0
설명: 32의 이진수는 100000 입니다. 
1과 1사이에 있는 0이 없으므로 0을 return

제출코드

def solution(N):
    arr = []
    count = 0
    max_count = 0
    arr = list(bin(N)[2:])
    for i in arr:
        if i == '0':
            count += 1
        if i == '1':
            if count > max_count:
                max_count = count
            count = 0
    print(max_count)
    return max_count

DAY2

문제

prices는 배열이며, 각 요소는 매일의 주식 가격입니다. 만약 한 번만 거래할 수 있다면 = 사고 팔 수 있다면, 제일 큰 이익은 얼마일까요?

예를 들어,

Input: [7,1,5,3,6,4]
Output: 5

설명: 2일(가격=1)에 샀다가 5일(가격=6)에 사는 것이 6-1이라 제일 큰 수익 7-1=6 은 안 되는거 아시죠? 먼저 사야 팔 수 있습니다.

Input: [7,6,4,3,1]
Output: 0

설명: 여기서는 매일 가격이 낮아지기 때문에 거래가 없습니다. 그래서 0

제출 코드

# 제일 작은 수를 먼저 찾고
# 배열을 그 뒤로 자름
# 자른 배열 안에서 가장 큰 값을 찾고
# 큰 값 - 작은 값 출력 
def maxProfit(prices):
    idx_min = prices.index(min(prices))
    new_arr = prices[idx_min:]
    return max(new_arr) - min(prices)

DAY3

문제

다음과 같이 input이 주어졌을 때, 같은 알파벳으로 이루어진 단어끼리 묶어주세요.

Input: ["eat", "tea", "tan", "ate", "nat", "bat"],
Output:
[
  ["ate","eat","tea"],
  ["nat","tan"],
  ["bat"]
]

output에서 순서는 상관없습니다.

사고과정

# unique set 리스트를 만들어서
# 해당하는 셋 리스트의 키값에 딕셔너리 밸류값으로 넣고 밸류만 출력 -> 잘 안됨
# unique set 리스트와 비교해서 같으면 새 리스트에 넣기

def groupanagrams(strs):
    strs_list = []
    unique_list = []
    answer_dict = {}
    for i in strs:
        strs_list.append(i)
        if i not in unique_list:
            unique_list.append(i)
    print(unique_list)

여기까지 쓰고 이후에 2시간정도 고민했는데 풀지 못해서 다른 블로그를 찾아봤다.

제출코드

출처 - Python: 알고리즘 - 딕셔너리 자료형

def groupAnagrams(strs):
    counter = {}

    for s in strs:
        # 정렬된 문자열을 key로 만들어준다.
        key = ''.join(sorted(s))
 
        if key not in counter:
            counter[key] = []
        # 그리고 문자열을 해당 key에 할당해준다.
        counter[key].append(s)
     
    result = []
    for key in counter:
        result.append(counter[key])
     
    return result 

DAY4

문제

숫자로 이루어진 리스트 nums를 인자로 주면, 그 안에서 어떤 연속적인 요소를 더했을 때 가장 큰 값이 나오나요? 가장 큰 값을 찾아 return해주세요.

Input: [-2,1,-3,4,-1,2,1,-5,4]
Output: 6

설명: [4,-1,2,1] 를 더하면 6이 가장 크기 때문

제출답안

def maxSubArray(nums):
  answers = []
  for m in range(0,len(nums)):
    for n in range(0,len(nums)):
      answers.append(sum(nums[m:n+1]))
      #m부터 n까지 합한 값 중에
  print(max(answers))
  #가장 큰 값을 출력
  return max(answers)

모범답안

def maxSubArray(nums):
    for i in range(1, len(nums)):
        if nums[i-1] > 0:
            nums[i] += nums[i-1] 
            print(nums)
    return max(nums)

위코드 한 달 백엔드 회고록

|

1차 프로젝트 데모 영상 & 인터뷰 영상

데모가 끝나고 2분 8초부터 팀원들 인터뷰가 나온다!

프로젝트 회고록 (감정)

팀의 분위기가 좋은게 정말 중요하다는걸 느꼈고 개인적으로는 실력이 없으니 여유가 없어졌어서 공부를 열심히, 열심히보다 잘 해야되겠다고 생가했다. 배려를 잘 해주시는 정말 좋은 팀원 분들을 만나서 덕을 많이 보았다!

퍼포먼스 코치 영은님이 처음에 ‘각자 자신이 잘하는 부분으로 팀에 기여하면 된다, 발표든 유머든 점심메뉴결정이든 서기이든 팀에 기여할 수 있는 부분이 있다.’고 하셨는데 팀원분들이 자원해서 최선을 다해주셨다. 특히 막내인 프론트엔드 분이 프론트엔드 팀장처럼 다른 분도 가르쳐주시고 했다.

내가 제안한 웹사이트가 선택되서 PM이 되었는데 회의진행 등의 PM업무 이외에 전체적인 그림상 필요한 자잘한 것들을 미리 챙기려고 했다(가짜데이터 업로드 등). 그럼에도 불구하고 실력 부족과 실수 연발로 어려움을 겪었어서 2차 프로젝트 때는 꼭 2번씩 체크하고! 조금씩 내가 맡은 부분을 완벽하게 해야겠다고 생각했다.

회원/ 비회원 별 다른 정보를 보여주는 부분에서 혼자 고민을 너무 오래 해서 프론트엔드 분들이 맞춰볼 수 있는 소중한 시간을 사용했다. 막히는 부분이 있으면 혼자 너무 오래 고민하기보다는 공부하고 구현해본다음 동료분들과 멘토님들께 지혜를 구하는 것이 좋겠다.

멘토님께 물어봤더니 바로 알려주셔서 그 이후로는 백엔드 단톡방도 만들고 이것저것 물어보고 다녔는데 아주 좋았다. 우리 팀에 잘하는 분은 다른 팀 코드도 읽어보고 칭찬하고 가서 적극적으로 설명도 듣고 하시길래 나도 그렇게 했는데 아주 많이 배웠다.

힘들 때도 있었는데 국비지원 수업 들으면서 프로젝트 했을 때보다는 정말 좋았고 특히 팀원분들이 좋으셔서 잘 마무리할 수 있었다.

프로젝트 회고록 (기술)

프로젝트가 시작하고나면 시간이 없으니 초반에 필요한 기능에 사용할 정보를 많이 공부해놓으면 좋겠다. 코드카타도 미리 해놓거나 아침에 일찍 와서 해놓는게 좋겠다.

프로젝트 하기 전에 어떤 웹사이트를 클론하면 어떤 기능이 필요한지 미리 생각해보고 웹서핑을 많이 해보면 좋았겠다.

  • 리뷰를 레스토랑 앱에 같이 넣을 걸, 앱을 여러 개로 따로 빼니까 URI짜기가 어려웠다.
  • 중간에 따로 있던 별점 테이블을 삭제하고 리뷰 테이블 안에 넣었다

장고 ORM의 Q객체, annotate, aggregate, filter(contains) 등등 여러가지 개념을 미리 공부해놓으면 좋겠다. 위스타그램할 때 시간이 남았는데 추가기능을 다 구현하면서 쿼리셋을 많이 가공해볼걸 싶었다.

백엔드 회고록

UX가 직관적이면서 아름다운 디자인을 좋아해서 처음에는 프론트엔드를 생각했는데 사전스터디때 알고리즘을 풀다가 파이썬이 깔끔하다는 생각이 들어서 백엔드를 선택했다.

기능구현은 되니까 안 되느냐의 문제라서 작업물의 정답이 정해져있다는 것이 좋다. 공부를 해서 머리를 싸매고 구현을 하면 결과물이 실력이 되고 누가 봤을 때 그 실력이 정직하게 보이는 것이 뿌듯하고 동기부여가 된다. 다른 말로하면 못하는 것도 숨길 수 없다..! 디자인보다 기능에 집중하고 최소한으로 작동하는 서비스(minimum viable product)를 구현하는 점이 좋다.

UX디자이너로 일할 때와 비교하자면 선택을 받지 않아도 되고 주관적인 가치판단에서 자유로워서 좋다.
하지만 잘하는 백엔드 동기 분들에 비해서 사고하고 코드를 작성하고 하는 능력이 좀 부족한 것 같아서 이걸 하는게 나한테 잘 맞는 것인지 싶은 생각도 든다.

아이디어

코드타카 하고나서 무작위로 몇 명씩 코드를 뽑아서 같이 살펴보고 설명하게 하고, 푸는 방법이 동영상으로 있었으면 좋겠다. 수강생이 짠 코드 중에 좋은 코드를 같이 볼 수 있었으면, 혹은 프로그래머스처럼 코드타카 코드들이 공유되서 수강생끼리 볼 수 있었으면 좋겠다.

쿼리셋 수업세션이 위스타그램 추가기능 구현하는 시간에 있었으면 좋았을 것 같다. 세션이 좀 더 많고 멘토님들이 너무 바쁘지 않았으면 좋겠다.

위코드의 한 달 생활 회고록

시스템이 좋아서 적극적으로 자기주도 학습을 하게 한다. 위코드의 철학, 교육공학이 훌륭하고 멘토님들, 매니저님이 정말정말 좋으시고 같은 기수의 동료분들도 다들 좋으셔서 캠프의 분위기가 좋고 공부가 잘 된다.

실력 차이가 많이 나서 따라가기 어려운 순간들이 좀 있었는데 6개월 전부터 알고리즘을 될 수 있는 대로 많이 풀어보고 왔으면 좋았을 것 같다.

위코드 첫번째 프로젝트 탱고플레이트 2 RESTful API, property등, , 최종 구현 영상

|

탱고플레이트 최종 구현 영상

view의 분류 기준

처음에 뷰 하나당 기능을 하나씩 넣었기 때문에 기능을 기준으로 뷰가 분류되는 것일까 생각했다. 장고 공식문서에 보면 이렇게 나와있다.

각각의 뷰는 파이썬 함수(클래스 기반의 뷰에서는 매쏘드)로서 대변되고 장고는 URL요청에 따라 뷰를 선택하게 될 것이다.

프로젝트를 진행하다보니 뷰를 리소스 기준으로 나누는 것이 URI를 봤을때 어떤 기능인 지 알 수 있게(restful한 API) 작성하기 좋았다.
예를 들어서 처음에는 레스토랑앱과 리뷰 앱을 따로 만들었는데 리뷰 페이지는 식당페이지에 붙어있었으니, 리뷰를 식당 앱에 같이 넣었으면 좋았겠다. 아래의 구조가 최적화된 뷰의 구조일 것 같다.

추천 엔드포인트

** 참고 RESTful API 작성 기준 **

리소스 POST GET PUT DELETE
/customers 새 고객 만들기 모든 고객 검색 고객 대량 업데이트 모든 고객 제거
/customers/1 Error 고객 1에 대한 세부 정보 검색 고객 1이 있는 경우 고객 1의 세부 정보 업데이트 고객 1 제거
/customers/1/orders 고객 1에 대한 새 주문 만들기 고객 1에 대한 모든 주문 검색 고객 1의 주문 대량 업데이트 고객 1의 모든 주문 제거

출처: RESTful web API 디자인

탱고플레이트 최종 uri (RESTful API)

#상위 프로젝트
urlpatterns = [
    path("users",include("users.urls")),
    path("",include("restaurants.urls")),
    path("restaurant",include("reviews.urls")),
]
#user 앱
urlpatterns = [
    path('/signup', SignUpView.as_view()),
    path('/signin', SignInView.as_view()),
    path('/restaurant/<int:restaurant_id>/wish', WishView.as_view()), 
    path('/wishlist', WishListView.as_view()),
    path("/signin/kakao/callback", KakaoSignIn.as_view()),
]
#review 앱
urlpatterns = [
    path('/<int:restaurant_id>/review', ReviewView.as_view()),
    path('/<int:restaurant_id>/review/<int:review_id>', ReviewView.as_view()),
]
#restaurant 앱
urlpatterns = [
    path("restaurant/<int:restaurant_id>", RestaurantDetailView.as_view()),
    path("", RestaurantListView.as_view()),
    path("search", SearchView.as_view()),
]

기억에 남는 코드 및 로직

쿼리셋 .last()

"restaurant_img" : restaurant.review_set.last().reviewimage_set.last().image,

쿼리셋 안의 여러 객체 중 마지막 객체를 불러온다.

aggregate

"rating"      : restaurant.review_set.all().aggregate(Avg('rating'))

이렇게 하면 리뷰테이블의 별점 컬럼의 모든 별점값의 평균을 구해주는데 자동으로 {rating__avg : 5} 이런식으로 반환되서 키값을 원하는 이름으로 바꾸고 숫자5만 받고 싶으면 아래와 같이 쓰면 된다.

"rating"         : restaurant.review_set.all().aggregate(rating = Avg('rating'))['rating']

annotate

"wish_count"     : WishList.objects.filter(restaurant_id = restaurant_id).annotate(cnt=Count('user_id')).count(),

위시리스트 테이블에서 레스토랑 아이디가 해당 값인 경우 컬럼을 한 개 더 만들어서 유저 아이디의 갯수를 세서 새로 만든 컬럼에 보여준다.

property 데코레이터 : 함수를 속성값으로 사용할 수 있게 해줌

from django.db    import models

class Restaurant(models.Model):
    name            = models.CharField(max_length=45)
    address         = models.CharField(max_length=100)
    phone_number    = models.CharField(max_length=50, null=True)
    location        = models.ForeignKey('Location',on_delete=models.CASCADE)
    category        = models.ForeignKey('Category', on_delete=models.CASCADE)
    serving_price   = models.ForeignKey('ServingPrice', on_delete=models.CASCADE)

    class Meta:
        db_table = 'restaurants'
    
    @property
    def latest_review(self):
        if not self.review_set.exists():
            return None

        return {
            "id"          : self.review_set.last().id,
            "user_id"     : self.review_set.last().user_id,
            "user_name"   : self.review_set.last().user.nickname,
            "description" : self.review_set.last().description,
            "image"       : self.review_set.last().reviewimage_set.last().image
        }

레스토랑 뷰에서 한 레스토랑의 가장 최근 리뷰를 불러오는 쿼리셋이 복잡해지고 있었는데 모델에서 이렇게 함수를 선언하고 @property 데코레이터로 속성(프로퍼티)으로 만들어놓으면 restaurant.latest_review 이런 식으로 바로 쓸 수 있다.

transaction.atomic

class ReviewView(View):
    @login_decorator
    @transaction.atomic
    def post(self, request, restaurant_id):
        try:
            data    = json.loads(request.body)
            user    = request.user

            if Review.objects.filter(user_id = user.id, restaurant_id=restaurant_id).exists():
                return JsonResponse({'MESSAGE':'REVIEW_EXIST'}, status=400)

            with transaction.atomic():
                review = Review.objects.create(
                    restaurant_id = restaurant_id,
                    user_id       = user.id,
                    description   = data['description'],
                    rating        = data['rating'],
                )

                with transaction.atomic():
                    ReviewImage.objects.create(
                        review_id = review.id,
                        image     = data['image'],
                        )

            return JsonResponse({'MESSAGE':'SUCCESS'},status = 201)
    
        except KeyError:
            return JsonResponse({'MESSAGE':'ERROR_INPUT_VALUE'}, status=404) 

리뷰와 리뷰이미지 테이블 2군데에 객체를 생성하는데 transaction.atomic을 사용하면 두 코드 중 하나가 실패했을 경우 첫번째 리뷰 객체만 생성되는 것이 아니라 첫번째가 롤백되서 둘 다 생성하지 않게된다.

비회원처리

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        try:
            token           = request.headers.get("authorization", None)
            if not token: # 토큰이 없으면 
                request.user = None #유저값에 none을 저장한다
            else:
                user            = jwt.decode(token, SECRET_KEY, algorithms='HS256')
                request.user    = User.objects.get(id = user['id'])

            return func(self, request, *args, **kwargs)

        except jwt.exceptions.DecodeError:
            return JsonResponse({"message" : "INVALID_TOKEN"}, status=400)

        except ObjectDoesNotExist:
            return JsonResponse({"message" : "INVALID_USER"}, status=400)

    return wrapper 

그런데 이렇게 처리하니까 프론트엔드에서 비회원은 header를 전달하지 않고 회원은 header를 전달하도록 짜야했는데 그 부분을 어떻게 할 수 있는지 모르겠다.

CSV 업로더

|

CSV파일 업로더

DB를 손으로 한땀한땀 넣을 수 없어서 위코드에서 제공하는 예리님의 튜토리얼을 보고 디비업로더 파일을 만들었다.

import os
import django
import csv
import sys

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tangoplate.settings")
django.setup()

from restaurants.models import Restaurant, Menu
from reviews.models import Review

CSV_PATH_LOCATION = 'review_images.csv'

def insert_restaurants():
    with open(CSV_PATH_LOCATION) as in_file:
        data_reader = csv.reader(in_file)
        for row in data_reader:
            Restaurant.objects.create(
                name = row[1], 
                address = row[2], 
                phone_number=row[3], 
                category_id = int(row[4]), 
                location_id = int(row[5]), 
                serving_price_id = int(row[6])
            )

def insert_menus():
    with open(CSV_PATH_LOCATION) as in_file:
        data_reader = csv.reader(in_file)
        for row in data_reader:
            Menu.objects.create(
                item=row[0], 
                item_price=row[1],
                restaurant_id=row[2]
            )

def insert_review():
    with open(CSV_PATH_LOCATION) as in_file:
        data_reader = csv.reader(in_file)
        for row in data_reader:
            Review.objects.create(
                description=row[0], 
                created_at=row[1],
                updated_at=row[2],
                restaurant_id=row[3],
                user_id=row[4],
            )

def insert_review_img():
    with open(CSV_PATH_LOCATION) as in_file:
        data_reader = csv.reader(in_file)
        for row in data_reader:
            Review.objects.create(
                image=row[0], 
                review_id=row[1],
            )

insert_review_img()

이렇게 하면 csv파일이 디비에 잘 올라간 것을 볼 수 있다. 이와 함께 sql workbench를 함께 사용하면 디비를 좀 더 보기쉽게 관리할 수 있다.

csv db

위코드 첫번째 프로젝트 탱고플레이트 1 ERD, models.py

|

망고플레이트 클론코딩

ERD 모델

erd

  • 레스토랑, 장소, 카테고리, 가격대 테이블

레스토랑에는 아이디, 이름, 주소, 전화번호 이외에 필터 기능을 만들기 위한 위치 아이디, 카테고리 아이디, 가격대 아이디를 넣었고 메뉴테이블과 연결해서 레스토랑마다 가지고 있는 메뉴와 해당 메뉴의 가격이 나오도록 했다.

  • 리뷰, 별점, 위시리스트 테이블

리뷰에는 레스토랑 아이디, 유저아이디, 리뷰 내용과 작성 날짜 및 수정날짜를 넣었다. 리뷰 한 개당 여러개의 사진이 들어갈 수 있도록 (일대다관계) 리뷰이미지 테이블을 따로 만들었다.
별점 테이블에는 레스토랑 아이디, 별점, 유저아이디를 넣었다.

별점만 줄 수도 있고 리뷰만 쓸 수도 있으니까 이렇게 잤는데 뷰를 짜다보니 리뷰와 별점이 한 테이블 안에 들어있지 않은 것이 좀 불편했다. 나중에 짤 때는 리뷰와 별점을 한 테이블에 넣고 리뷰를 남기려면 별점도 함께 입력해야 되도록 하는게 좋을 것 같다.

위시리스트에는 가고싶다가 클릭되었을때 열이 생성되고 가고싶다가 또 클릭되었을 때 해당 열이 지워지도록 레스토랑 아이디와 유저아이디를 넣었다.

2021.08.13 -> 리뷰를 받고 별점을 리뷰테이블에 넣는걸로 수정했다. 테이블을 여러 개 만드는 것보다 컬럼을 여러 개 넣는 것이 비용이 덜 든다.

  • 쿠폰(잇딜) 테이블

쿠폰 테이블에는 레스토랑 아이디, 가격, 설명, 사용기간을 넣고 쿠폰 역시 사진이 일대다관계로 연결될 것이라서 쿠폰 이미지 테이블을 따로 만들었는데 망고플레이트의 잇딜 페이지를 보면 잇딜1개당 사진이 1개 들어있어서 이미지를 쿠폰 테이블에 같이 넣었어도 되었을 것 같다.

models.py

장고 앱 내에 coupons, restaurants, reviews, users 앱을 네개 만들고 각각 해당 모델을 넣었다.

  • users : User, Rating, WishList
from django.db          import models

class User(models.Model):
    nickname     = models.CharField(max_length=50, null=True)
    email        = models.EmailField(max_length=200)
    password     = models.CharField(max_length=300)
    #비밀번호는 암호화되기 때문에 길게 주어야한다.
    phone_number = models.CharField(max_length=45)

    class Meta:
        db_table = 'users'

class Rating(models.Model):
    user       = models.ForeignKey('User', on_delete=models.SET_NULL, null=True)
    #참조하는 유저가 지워져도 별점 데이터는 남는다.
    restaurant = models.ForeignKey('restaurants.Restaurant', on_delete=models.CASCADE)
    #참조하는 레스토랑이 지워지면 별점 데이터가 지워진다.
    rating     = models.IntegerField()

    class Meta:
        db_table = 'ratings'

class WishList(models.Model):
    user       = models.ForeignKey('User', on_delete=models.SET_NULL, null=True)
    #참조하는 유저가 지워져도 위시리스트 데이터는 남는다.
    restaurant = models.ForeignKey('restaurants.Restaurant', on_delete=models.CASCADE)
    #참조하는 레스토랑이 지워지면 위시리스트 데이터가 지워진다.

    class Meta:
        db_table = 'wishlist'
  • restaurants : Restaurant, Menu, Location, Category, ServingPrice
from django.db    import models

class Restaurant(models.Model):
    name            = models.CharField(max_length=45)
    address         = models.CharField(max_length=100)
    phone_number    = models.CharField(max_length=50, null=True)
    location        = models.ForeignKey('Location',on_delete=models.CASCADE)
    category        = models.ForeignKey('Category', on_delete=models.CASCADE)
    serving_price   = models.ForeignKey('ServingPrice', on_delete=models.CASCADE)

    class Meta:
        db_table = 'restaurants'

class Menu(models.Model):
    restaurant     = models.ForeignKey('Restaurant', on_delete=models.CASCADE)
    item           = models.CharField(max_length=45)
    item_price     = models.DecimalField(max_digits= 10, decimal_places=0)
    #처음에 6으로 했더니 10만원이상이 입력이 안 되서 10으로 바꿨는데 PositiveIntegerField()로 했어도 될 것 같다. 

    class Meta:
        db_table = 'menus'

class Location(models.Model):
    area = models.CharField(max_length=45)

    class Meta:
        db_table = 'locations'

class Category(models.Model):
    name = models.CharField(max_length=45)

    class Meta:
        db_table = 'categories'

class ServingPrice(models.Model):
    price = models.PositiveIntegerField()
    #가격이라서 양수 정수 필드를 사용한다.

    class Meta:
        db_table = 'serving_prices'

  • reviews : Review, ReviewImage
from django.db          import models

class Review(models.Model):
    user        = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True)
    restaurant  = models.ForeignKey('restaurants.Restaurant', on_delete=models.CASCADE)
    description = models.CharField(max_length=500, null=True)
    created_at  = models.DateField(auto_now_add=True)
    #생성날짜는 자동으로 생기도록 했다.
    updated_at  = models.DateField(auto_now=True, null=True)

    class Meta:
        db_table = 'reviews'

class ReviewImage(models.Model):
    review     = models.ForeignKey('Review',on_delete=models.SET_NULL, null=True)
    image      = models.URLField(max_length=500)
    #500이면 충분하다고 생각했는데 url은 하도 길어서 길이 제한에 걸리는 경우가 종종 있었다.
    
    class Meta:
        db_table = 'review_images'
  • coupon : Coupon, CouponImage
from django.db          import models

class Coupon(models.Model):
    name           = models.CharField(max_length=40)
    restaurant     = models.ForeignKey('restaurants.Restaurant', on_delete=models.SET_NULL, null=True)
    price          = models.DecimalField(max_digits= 6, decimal_places=0)
    description    = models.CharField(max_length=500, null=True)
    start_date     = models.DateField()
    end_date       = models.DateField()

    class Meta:
        db_table = 'coupons'

class CouponImage(models.Model):
    coupon        = models.ForeignKey('Coupon', on_delete=models.SET_NULL, null=True)
    image         = models.URLField(max_length=500)

    class Meta:
        db_table = 'coupon_images'

2주간 망고플레이트를 클론코딩하면서 시간상 잇딜부분은 구현하지 못했고 마지막으로 아래와 같은 erd를 사용하였다. 탱고플레이트 최종 erd

1차에서는 1유저당 1식당에 1리뷰만 넣을 수 있게 했고, 1리뷰당 1리뷰이미지를 넣도록 했다.
쿼리셋에서 맨 마지막 쿼리객체를 가져오는.last()라는 구문도 알게 되었고 restaurant.first_review() 이렇게 불러오기만 하면 되는 @property라는 것도 알게 되었고 list comprehension도 삼항연산자도 익숙해졌으니 2차프로젝트에서는 좀 더 실제 서비스에 가깝게 제한사항 없이 만들어보고 싶다.

코드카타 3주차

|

DAY1

문제

두 개의 input에는 복소수(complex number)가 string 으로 주어집니다. 복소수란 a+bi 의 형태로, 실수와 허수로 이루어진 수입니다.

input으로 받은 두 수를 곱해서 반환해주세요. 반환하는 표현도 복소수 형태의 string 이어야 합니다.

복소수 정의에 의하면 (i^2)는 -1 이므로 (i^2) 일때는 -1로 계산해주세요.

  • 제곱 표현이 안 되어 i의 2제곱을 (i^2)라고 표현했습니다.

예제 1:

Input: “1+1i”, “1+1i”
Output: “0+2i”
설명:
(1 + i) * (1 + i) = 1 + i + i + i^2 = 2i
2i를 복소수 형태로 바꾸면 0+2i.

예제 2:

Input: “1+-1i”, “1+-1i”
Output: “0+-2i”
설명:
(1 - i) * (1 - i) = 1 - i - i + i^2 = -2i,
-2i를 복소수 형태로 바꾸면 0+-2i.

예제 3:

Input: “1+3i”, “1+-2i”
Output: “7+1i”
설명:
(1 + 3i) * (1 - 2i) = 1 - 2i + 3i -6(i^2) = 1 + i + 6,
7+i를 복소수 형태로 바꾸면 7+1i.

가정

input은 항상 a+bi 형태입니다.
output도 a+bi 형태로 나와야 합니다.

제출코드

def complex_number_multiply(a, b):
    #(A+Bi)*(C+Di)
    list_a = a.split('+')
    A = int(list_a[0])
    B = int(list_a[-1][:-1])

    list_b = b.split('+')
    C = int(list_b[0])
    D = int(list_b[-1][:-1])

    num1 = str(A*C - B*D)
    num2 = str(A*D + B*C)

    return num1 +'+'+(num2 + 'i')

DAY2

문제

문자로 구성된 배열을 input으로 전달하면, 문자를 뒤집어서 return 해주세요.

  • 새로운 배열을 선언하면 안 됩니다.
  • 인자로 받은 배열을 수정해서 만들어주세요.
Input: ["h","e","l","l","o"]
Output: ["o","l","l","e","h"]

Input: ["H","a","n","n","a","h"]
Output: ["h","a","n","n","a","H"]

제출 코드

def reverse_string(s):
    print (list(reversed(s)))
    return list(reversed(s))

DAY3

문제

양수로 이루어진 m x n 그리드를 인자로 드립니다.
상단 왼쪽에서 시작하여, 하단 오른쪽까지 가는 길의 요소를 다 더했을 때,가장 작은 합을 찾아서 return 해주세요.

한 지점에서 우측이나 아래로만 이동할 수 있습니다.

Input: [    [1,3,1],    [1,5,1],    [4,2,1] ]

Output: 7

설명: 1→3→1→1→1 의 합이 제일 작음

제출코드

이 문제는 풀지 못하고 다른 블로그를 찾아봤다.

https://velog.io/@langssi/Python-Code-Kata-Day13

def min_path_sum(grid):
  m = len(grid)
  n = len(grid[0])
    
  ### 1
  for i in range(1, n):
      grid[0][i] += grid[0][i-1]
  for i in range(1, m):
      grid[i][0] += grid[i-1][0]
        
  ### 2
  for i in range(1, m):
      for j in range(1, n):
          grid[i][j] += min(grid[i-1][j], grid[i][j-1])
  return grid[-1][-1] 

DAY4

문제

주어진 숫자 배열에서, 0을 배열의 마지막쪽으로 이동시켜주세요. 원래 있던 숫자의 순서는 바꾸지 말아주세요.

  • 새로운 배열을 생성해서는 안 됩니다.
Input: [0,1,0,3,12]
Output: [1,3,12,0,0]

제출코드

def move_zeroes(nums):
    num_of_zero = nums.count(0)
    for i in range(num_of_zero):
      nums.remove(0)
      nums.append(0)
    return nums

DAY5 재귀 알고리즘

오늘은 재귀알고리즘에 대한 문제입니다. 재귀(recursion)란, 자신을 정의할 때 자기 자신을 호출하는 방법을 뜻합니다. 프로그래밍의 함수정의에서 많이 사용됩니다.

예)

def countdown(n):
  print(n)
  countdown(n-1)

countdown(10);

countdown 함수는 받은 인자를 출력합니다. 그런데 위의 함수를 실행하면 10에서 시작해서 무한으로 마이너스 값까지 내려갑니다.

그래서 재귀함수는 아래의 절차가 꼭 필요합니다. 언제 멈출것인가?

위를 고려해 0이 되면 더이상 재귀를 이어나가지 않도록 종료 조건을 추가하겠습니다.

def countdown(n):
  print(n)
  
  if (n == 0):
    return None
  
  countdown(n-1)

countdown(10);

재귀의 이론은 위와 같이 아주 간단합니다. 재귀를 더 공부하고 싶은 분은 인터넷에 재귀 문제를 찾아 더 풀어보셔도 좋고, 알고리즘 책에서 재귀 부분만 더 읽으셔도 좋습니다.

문제

재귀를 사용하여 팩토리얼(factorial)을 구하는 함수를 구현해주세요. 팩토리얼이란 1에서부터 n까지의 정수를 모두 곱한것을 말합니다.

1! = 1 2! = 1 * 2 5! = 1 * 2 * 3 * 4 * 5

제출코드

def factorial(n):
    if n <= 1:
      return 1
    return n * factorial(n-1)

코드카타 2주차

|

DAY1

문제

로마자에서 숫자로 바꾸기 1~3999 사이의 로마자 s를 인자로 주면 그에 해당하는 숫자를 반환해주세요.

로마 숫자를 숫자로 표기하면 다음과 같습니다.

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

로마자를 숫자로 읽는 방법은 로마자를 왼쪽부터 차례대로 더하면 됩니다. III = 3 XII = 12 XXVII = 27입니다.

그런데 4를 표현할 때는 IIII가 아니라 IV 입니다. 뒤의 숫자에서 앞의 숫자를 빼주면 됩니다. 9는 IX입니다.

I는 V와 X앞에 와서 4, 9 X는 L, C앞에 와서 40, 90 C는 D, M앞에 와서 400, 900

제출코드

def roman_to_num(s):
  roman = {
    'I' : 1,
    'V' : 5,
    'X' : 10,
    'L' : 50,
    'C' : 100,
    'D' : 500,
    'M' : 1000
  }
  a = 0
  for i in range(len(s)):
    a += roman[s[i]]
  if 'IV' in s or 'IX' in s:
    a -= 2
  if 'XL' in s or 'XC' in s:
    a -= 20
  if 'CD' in s or 'CM' in s:
    a -= 200
  return a

DAY2

문제

숫자로 이루어진 배열인 nums를 인자로 전달합니다.

숫자중에서 과반수(majority, more than a half)가 넘은 숫자를 반환해주세요.

예를 들어,

nums = [3,2,3]
return 3

nums = [2,2,1,1,1,2,2]
return 2

가정

nums 배열의 길이는 무조건 2 이상입니다.

사고과정

유니크한 숫자 세트 리스트 만들고, 각 숫자가 나온 개수를 카운트하고 두 개를 zip으로 묶어준다. 카운트가 과반수 이상이면 해당 키를 반환한다.

import statistics

def more_than_half(nums):
    key_list = list(set(nums))
    value_list = []
    for i in key_list:
        value_list.append(nums.count(i))
    arr = list(zip(key_list, value_list))
    return key_list[value_list.index(max(value_list))]  
    # arr = {x:y for x,y in zip(key_list, value_list)}

자주나온 숫자를 많이 나온 순서대로 뽑아주는 카운터 내장함수를 사용하면 빠르고 간편하게 답을 찾을 수 있다.

from collections import Counter

def more_than_half(nums):
  return Counter(nums).most_common(1)[0][0]
  # 카운터 매쏘드 내에 most_common함수를 사용하면 (n)개가 순서대로 출력됨

DAY3

문제

s는 여러 괄호들로 이루어진 String 인자입니다. s가 유효한 표현인지 아닌지 true/false로 반환해주세요.

종류는 ‘(‘, ‘)’, ‘[’, ‘]’, ‘{‘, ‘}’ 으로 총 6개 있습니다. 아래의 경우 유효합니다.

한 번 괄호를 시작했으면, 같은 괄호로 끝내야 한다. 괄호 순서가 맞아야 한다.

예를 들어 아래와 같습니다.

s = "()"
return true

s = "()[]{}"
return true

s = "(]"
return false

s = "([)]"
return false

s = "{[]}"
return true

제출 코드

def is_valid(string):
    paren_dict = {'(':')', '{':'}', '[':']'}
    temp = []
    for ch in string:
      if ch in list(paren_dict.keys()):
        #괄호가 열렸다면
        temp.append(ch)
        #열린 괄호를 템프에 넣는다
      else:
        #괄호가 열리지 않았다면
        if not temp:
          #템프리스트가 비어있다면(열린 적 없음)
          return False
        else:
          #템프리스트 안에 뭔가 들어있다면
          if ch != paren_dict[temp.pop()]:
            #템프리스트의 가장 마지막 원소를 빼낸 것이 parent_dict의 닫힌괄호와 짝이 맞지 않는다면
            return False
        if temp: #템프리스트 안에 내용물이 남아있다면
            return False
    return True

DAY4

문제

nums는 숫자로 이루어진 배열입니다.

가장 자주 등장한 숫자를 k 개수만큼 return 해주세요.

nums = [1,1,1,2,2,3],
k = 2

return [1,2]

nums = [1]
k = 1

return [1]

제출코드

def most_common(nums,k):
    num_list = []
    ans_list = []
    for i in set(nums):
        num_list.append(nums.count(i))  
    ans_list = list(zip(num_list, set(nums)))
    ans_list = sorted(ans_list)
    print(ans_list)
    answer = []
    for i in range(k):
        answer.append((ans_list.pop()[1]))
        print(answer)
    return answer

DAY5

문제

인자인 height는 숫자로 이루어진 배열입니다.그래프로 생각한다면 y축의 값이고, 높이 값을 갖고 있습니다.

아래의 그래프라면 height 배열은 [1, 8, 6, 2, 5, 4, 8, 3, 7] 입니다.

Graph

저 그래프에 물을 담는다고 생각하고, 물을 담을 수 있는 가장 넓은 면적의 값을 반환해주세요.

가정

배열의 길이는 2이상입니다.

사고과정

첫번째로 위치한 높은 막대와 두번쨰로 위치한 높은 막대를 찾는다. 둘 사이의 거리 * 두번째로 높은 막대의 높이 : 담기는 물의 면적
이라고 생각했는데 틀렸다.
낮은 막대라도 면적이 더 넓을 수 있기 때문에 모든 두 막대 조합의 넓이를 구하고 그 중 가장 넓은 면적을 반환하는 것이 맞는 방법이다.

제출코드

def get_max_area(height):
    result = []

    for i in range(len(height)-1):
        for j in range(i+1, len(height)):
            if height[i] > height[j]:
                result.append(height[j]*(j-i))
            else:
                result.append(height[i]*(j-i))
    print(max(result))
    return max(result)