루나의 TIL 기술 블로그

포스팅 예약발행

|

목적

원하는 시간에 포스팅을 발행한다.

초본

/batch/v3_1/batch_reserve_publish.py

import pandas as pd
import pymysql
from apis.v3_1.community import save_board_data
from datetime import datetime
from config.config_db import DataBaseConfig as con_DB
from configuration    import server_name, send_slack

def reserve_publish(conn):
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    sql = f"""
        select reserve_dt, reserve_seq
          from db_community.tb_board_reserve
         where del_yn = 0
    """
    df_reserve = pd.read_sql(sql, conn)

    reserve_list = []

    now = datetime.now().strftime('%Y-%m-%d-%H:%M:%S')

    for row in df_reserve:
        if now == row[0]:
            reserve_list.append(row[1])

    for reserve_seq in reserve_list:
        sql = f"""
            select category, title, contents, user_seq, video_yn, video_link
              from db_community.tb_board_reserve
             where reserve_seq = {reserve_seq}
        """
        cursor.execute(sql)
        args = cursor.fetchone()

        user_seq = args['user_seq']
        title = args['title']

        save_board_data(conn, args, 'post')

        send_slack(f' {now}{user_seq}번 유저가, \n'
                   f' {title} , \n'
                   f' 글을 예약발행했습니다',
                   f' tangopick_{server_name}_reserve')
    return True

if __name__ == '__main__':
    conn = pymysql.connect(**con_DB.tangopick_db_config(read=False)[0])
    reserve_publish(conn)
    conn.commit()
    conn.close()

피드백

함수 밖에서 시간을 받아서 해당 시간에 예약발행해야하는 건만 sql에서 뽑는것이 좋겠다. 예외처리구문을 사용해서 fail나는 건을 잡아서 3분 후 다시 돌려주고, 2번 fail나는 경우 슬랙을 발송한다.

중간 수정본

import pymysql
from apis.v3_1.community import save_board_data
from datetime            import datetime
from config.config_db    import DataBaseConfig as con_DB
from configuration       import server_name, send_slack

def reserve_publish(conn, now):
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    sql = f"""
        select reserve_dt, reserve_seq, category, title, contents, user_seq, is_public, video_yn, video_link, comm_seq 
          from db_community.tb_board_reserve
         where reserve_dt = '{now}' and del_yn = 0
    """
    cursor.execute(sql)
    rows = cursor.fetchall()

    cnt = 0
    fail_cnt = 0

    try:
        for args in rows:
            #발행할 때 없어야되서 삭제
            args.pop('reserve_dt')

            save_board_data(conn, args, 'POST')
            cnt = cnt+1

            #발행하면 예약에서 삭제
            delete_sql = f"""
                update db_community.tb_board_reserve
                 where reserve_seq = {args['reserve_seq']}
                   set del_yn = 0, del_dt = {now} 
            """
            cursor.execute(delete_sql)
            print(args['reserve_seq'],'삭제되었습니다')

    except Exception as e:
        print(str(e))
        fail_cnt = fail_cnt + 1

    if cnt != 0:
        send_slack(f' {cnt}개의 포스팅이 \n'
                   f' {now}에 예약발행되었습니다 \n',
                   f' tangopick_{server_name}_reserve')

    if fail_cnt != 0:
        send_slack(f' {fail_cnt}개 실패했습니다 \n',
                   f' tangopick_{server_name}_reserve')

    return True

if __name__ == '__main__':
    conn = pymysql.connect(**con_DB.tangopick_db_config(read=False)[0])

    now = datetime.now()
    now_str = now.strftime('%Y-%m-%d %H:%M:00')
    now = datetime.strptime(now_str, '%Y-%m-%d %H:%M:00')

    reserve_publish(conn, now)
    conn.commit()
    conn.close()

수정사항

몽고디비를 연결해서 게시글 작성할 때 입력된 태그를 저장한다.

최종본

import pymysql
from pymongo import MongoClient
import copy
from datetime             import datetime
from config.config_db     import DataBaseConfig as con_DB
from config.config_doc_db import DocumentDB as con_DocDB
from configuration        import server_name, send_slack
from apis.v3_1.community  import save_board_data


def reserve_publish(conn, conn_mongo, now):
    #몽고DB user 데이터베이스에 연결
    doc_db = conn_mongo.user
    #Mysql연결
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    sql = f"""
        select reserve_dt, reserve_seq, category, title, contents, user_seq, is_public, video_yn, video_link, comm_seq 
          from db_community.tb_board_reserve
         where reserve_dt = '{now}' and del_yn = 0
    """
    cursor.execute(sql)
    rows = cursor.fetchall()

    cnt = 0
    fail_cnt = 0

    try:
        for args in rows:
            #발행할 때 없어야되서 삭제
            args.pop('reserve_dt')

            #발행
            result = save_board_data(conn, args, 'POST')
            contents = result['contents']

            #태그 몽고디비 저장
            board_contents = {'contents': contents, 'board_seq': result['board_seq']}
            success = save_doc_db(db='community', table='board_contents', key='board_seq', args=board_contents,
                                  method='POST')
            success = save_doc_db(db='community', table='board', key='board_seq', args=result, method='POST')
            print(f'doc_db 저장 완료')

            if success:
                sql = f"""
                update db_community.tb_community
                set upd_ts = unix_timestamp()
                where comm_seq =  {args['comm_seq']}
                """
                cursor.execute(sql)

            #슬랙 발송시 표시되는 발행된 게시글 갯수
            cnt = cnt + 1

            #발행하면 예약에서 삭제
            delete_sql = f"""
                update db_community.tb_board_reserve 
                   set del_yn = 1, del_dt = now()
                 where reserve_seq = {args['reserve_seq']} 
            """
            cursor.execute(delete_sql)
            print(args['reserve_seq'],'삭제되었습니다')

    except Exception as e:
        print(str(e))
        fail_cnt = fail_cnt + 1

    if cnt != 0:
        send_slack(f' {cnt}개의 포스팅이 \n'
                   f' {now}에 예약발행되었습니다 \n',
                   f' tangopick_{server_name}_reserve')

    if fail_cnt != 0:
        send_slack(f' {fail_cnt}개 실패했습니다 \n',
                   f' tangopick_{server_name}_reserve')

    return True

def save_doc_db(db, table, key, args, method):
    database = conn_mongo[db]
    collection = database[table]

    if method == 'DELETE':
        collection.delete_one({"_id": args[key]})
        print('doc_db 삭제 성공')
        return

    post_id = None
    # print('args', args)
    doc_dict = copy.deepcopy(args)
    doc_dict['_id'] = int(args[key])
    doc_dict['del_yn'] = 0

    if method == 'POST':
        post_id = collection.insert_one(doc_dict)
        print(f'doc_db {table} 저장 성공', post_id)
    else:
        post_id = collection.save(doc_dict)
        print(f'doc_db {table} 수정 성공', post_id)

    return True

if __name__ == '__main__':
    conn = pymysql.connect(**con_DB.tangopick_db_config(read=False)[0])
    conn_mongo = MongoClient(**con_DocDB.DOC_DB_INFO, **{'retryWrites': False})

    now = datetime.now()
    now_str = now.strftime('%Y-%m-%d %H:%M:00')
    now = datetime.strptime(now_str, '%Y-%m-%d %H:%M:00')

    reserve_publish(conn, conn_mongo, now)
    conn.commit()
    conn.close()

회고

몽고디비를 연결하는 내용이 알고나니 매우 쉬웠는데 모를 때는 정말 어렵게 느껴졌었다.

파이썬 크롤링시 한글 깨짐, 엔코딩 덮어씌우기

|

목적

사용자가 게시판에 링크 입력 시 뉴스 페이지에서 제목과 썸네일 등을 끌어올 때 한글 깨짐을 예방한다.

해결방법: ‘EUC-KR’ 엔코딩을 덮어씌운다


headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get(url, headers=headers, timeout=3)

if data.encoding not in ['euc-kr', 'utf-8']:
    data.encoding = 'EUC-KR'

soup = BeautifulSoup(data.text, 'html.parser')

title = soup.select_one('meta[property="og:title"]')
image = soup.select_one('meta[property="og:image"]')
desc = soup.select_one('meta[property="og:description"]')

시행착오의 흔적


charset = soup.select_one('meta[http-equiv="Content-Type"]')
print('charset', charset)

charset2 = charset.attrs("charset")
print('charset2', charset2)

# charset.get('content')[charset.get('content').find("charset="):]

charset3 = charset.get('content')[charset.get('content').find("charset="):].replace('charset=', '')
print('charset3', charset3)

charset4 = soup.headers.get_content_charset()
print('charset4', charset4)

# res = req.urlopen(url).read().decode(req.urlopen(url).headers.get_content_charset())
# req.urlopen(url).headers.get_content_charset()

# soup = BeautifulSoup(data.content.decode('utf-8', 'replace'), 'html.parser')


메타태그와 api를 사용해서 유투브 동영상 정보 끌어오기

|

목적

사용자에게 입력받은 여러 형태의 유투브 비디오 링크로 썸네일, 길이, 비디오 크기, 채널이름, 제목, 설명을 끌어와서 사용한다.

1. 입력받은 링크에서 키를 추출한다.

import re
import json
from urllib.request import urlopen
from urllib.parse import urlparse, parse_qs
from contextlib import suppress
from configuration import yt_api_key

def get_yt_id(url, ignore_playlist=False):
    # Examples:
    # - http://youtu.be/SA2iWivDJiE
    # - http://www.youtube.com/watch?v=_oPAwA_Udwc&feature=feedu
    # - http://www.youtube.com/embed/SA2iWivDJiE
    # - http://www.youtube.com/v/SA2iWivDJiE?version=3&hl=en_US
    query = urlparse(url)
    if query.hostname == 'youtu.be': return query.path[1:]
    if query.hostname in {'www.youtube.com', 'youtube.com'}:
        if not ignore_playlist:
        # use case: get playlist id not current video in playlist
            with suppress(KeyError):
                return parse_qs(query.query)['list'][0]
        if query.path[:7] == '/shorts': return query.path.split('/')[2]
        if query.path == '/watch': return parse_qs(query.query)['v'][0]
        if query.path[:7] == '/watch/': return query.path.split('/')[1]
        if query.path[:7] == '/embed/': return query.path.split('/')[2]
        if query.path[:3] == '/v/': return query.path.split('/')[2]

   # returns None for invalid YouTube url

def get_yt_duration(url):
    video_id = get_yt_id(url)
    video_url = "https://www.googleapis.com/youtube/v3/videos?id=" + video_id + "&key=" + yt_api_key + "&part=contentDetails&part=snippet"
    response_video = urlopen(video_url).read()
    data_video = json.loads(response_video)

    channel = data_video['items'][0]['snippet']['channelTitle']
    duration = data_video['items'][0]['contentDetails']['duration']
    reg_dt = data_video['items'][0]['snippet']['publishedAt']
    thumbnail = data_video['items'][0]['snippet']['thumbnails']['medium']['url']

    match = re.match('PT(\d+H)?(\d+M)?(\d+S)?', duration).groups()
    hours = _js_parseInt(match[0]) if match[0] else 0
    minutes = _js_parseInt(match[1]) if match[1] else 0
    seconds = _js_parseInt(match[2]) if match[2] else 0
    if hours:
        if len(str(minutes)) == 1:
            minutes = '0' + str(minutes)
        duration = str(hours) + ':' + str(minutes) + ':' + str(seconds)
    else:
        if len(str(seconds)) == 1:
            seconds = '0' + str(seconds)
        duration = str(minutes) + ':' + str(seconds)

    return {'channel': channel, 'duration': duration, 'reg_dt': reg_dt, 'thumbnail': thumbnail}

# js-like parseInt
def _js_parseInt(string):
    return int(''.join([x for x in string if x.isdigit()]))


if __name__ == '__main__':
    #추출하고 싶은 링크를 입력하고 파일을 돌린다.
    link = 'https://youtu.be/NSAzhdHeHK4'
    print(get_yt_id(link))
    print(get_yt_duration(link))

키 추출하는 부분 출처 : https://stackoverflow.com/questions/4356538/how-can-i-extract-video-id-from-youtubes-link-in-python

2. 키를 이용해서 api를 치고 영상길이와 채널이름을 추출한다.

def get_yt_duration(url):
    results = {}
    video_id = get_yt_id(url)
    video_url = "https://www.googleapis.com/youtube/v3/videos?id=" + video_id + "&key=" + yt_api_key + "&part=contentDetails&part=snippet"
    response_video = urlopen(video_url).read()
    data_video = json.loads(response_video)

    channel = data_video['items'][0]['snippet']['channelTitle']
    duration = data_video['items'][0]['contentDetails']['duration']

    match = re.match('PT(\d+H)?(\d+M)?(\d+S)?', duration).groups()
    hours = _js_parseInt(match[0]) if match[0] else 0
    minutes = _js_parseInt(match[1]) if match[1] else 0
    seconds = _js_parseInt(match[2]) if match[2] else 0
    if hours:
        if len(str(minutes)) == 1:
            minutes = '0' + str(minutes)
        duration = str(hours)+':'+str(minutes)+':'+str(seconds)
    else:
        if len(str(seconds)) == 1:
            seconds = '0' + str(seconds)
        duration = str(minutes) + ':' + str(seconds)

    return {'channel' : channel, 'duration' : duration}

# js-like parseInt
def _js_parseInt(string):
    return int(''.join([x for x in string if x.isdigit()]))

참조 : https://stackoverflow.com/questions/16742381/how-to-convert-youtube-api-duration-to-seconds

3. soup으로 메타태그에서 썸네일, 제목, 설명, 태그, 비디오 크기 가져온다.

@ns.route("/videolink")
@ns.doc(responses={200: 'Success', 300: 'Redirected', 400: 'Invalid Argument', 500: 'Mapping Key Error'})
@ns.expect(video_model)
class ParsingVideoUpload(Resource):
    @jwt_required
    def post(self):
        """
        동영상 링크 입력
        :return:
        """
        conn = g.db
        args = request.json
        url = args['video_link']

        try:
            # 링크도 아닌 문자열같은게 들어오지는 않았는지 검사한다.
            assert 'youtu' in url, '영상포스팅에는 유튜브 링크만 첨부 가능합니다'
            # 유효한 링크인지 연결을 검사하고
            res = urlopen(url)
            print('****res.status :', res.status)
            # 헤더를 넣어주면 크롤링으로 인식하지 않아서 403에러가 나지 않는다.
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
            data = requests.get(url, headers=headers, timeout=3)
            soup = BeautifulSoup(data.text, 'html.parser')
            
            #soup으로 사이트 이름 메타태그를 뽑아서 유튜브인지 확인하고
            if soup.select_one('meta[property="og:site_name"]')['content'] == "YouTube":

                # 주소에서 아이디 추출
                youtube_id = get_yt_id(url)
                # api로 영상길이와 채널이름 추출
                youtube_data = get_yt_duration(url)
                video_duration = youtube_data['duration']
                channel_title = youtube_data['channel']

                video_thumbnail = 'https://img.youtube.com/vi/'+ youtube_id +'/mqdefault.jpg'
                video_tags = soup.select_one('meta[name="keywords"]')['content']
                video_title = soup.select_one('meta[property="og:title"]')['content']
                video_desc = soup.select_one('meta[property="og:description"]')['content']
                video_width = soup.select_one('meta[property="og:video:width"]')['content']
                video_height = soup.select_one('meta[property="og:video:height"]')['content']

                #썸네일 추출 안 되었으면 고정값 삽입
                if not video_thumbnail:
                    print("동영상에서 썸네일 이미지를 불러올 수 없습니다.")
                    video_thumbnail = 'https://cloudfront.alpha-bridge.kr/dev/image/board/youtube_thumbnail_default.png'

                data = {
                    'youtube_id': youtube_id,
                    'channel' : channel_title,
                    'video_duration': video_duration,
                    'thumbnail_img': video_thumbnail,
                    'video_tags': video_tags,
                    'video_title': video_title,
                    'video_desc': video_desc,
                    'video_width': video_width,
                    'video_height': video_height
                }

                return {'result': 'success', 'data': data}

        except HTTPError as e:
            # 유효하지 않으면 에러 출력
            err = e.read()
            code = e.getcode()
            print('****code :', code)  ## 404

            return {'result': 'failed', 'err_code' : code}

google youtube api quota(할당량) 관련

영상포스팅 작성시 입력, 발행, 수정할 때에 api를 요청하므로 글 1개 작성할 때 2번의 api를 요청하게 되고 할당량은 10정도 사용하는 것 같다.(사진 및 링크 참조) 현재 할당량은 하루에 만건이므로 1000건 정도의 생성 및 수정을 감당할 수 있다. 할당량은 추가로 요청하면 되는데 5주정도 걸린다고 한다.

참고: https://developers.google.com/youtube/v3/determine_quota_cost

api 사용 화면

유투브 api 키등록 관련 참고 : https://datadoctorblog.com/2021/09/12/etc-google-cloud-platform-youtube/ 유투브 api json 구조 관련 공식 문서 : https://developers.google.com/youtube/v3/docs/videos

여러 테이블 상하로 붙여서 view생성하기

|

목적

여러 테이블에 나눠져있는 연결되지 않은 정보를 한 개의 뷰로 모아서 볼 수 있도록 하고 싶었다.

mysql 코드

create view 최근2달신고 as
select 'comment', a.comment_seq 신고받은번호, u.nickname 신고자, a.reg_dt 신고일, g.comment 내용, a.REPORT_TYPE, a.REPORT_REASON
  from db_community.tb_board_comment_report a left join db_user.tb_user_info u
                                                     on u.user_seq = a.user_seq
                                              left join db_community.tb_board_comment g
                                                     on a.COMMENT_SEQ = g.comment_seq
 where a.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY)  
union all
select 'board', b.board_seq, u.nickname, b.reg_dt, h.TITLE, b.REPORT_TYPE, b.REPORT_REASON
  from db_community.tb_board_report b left join db_user.tb_user_info u
                                             on u.user_seq = b.user_seq
                                      left join db_community.tb_board h
                                             on b.BOARD_SEQ = h.board_seq
 where b.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY)
union all
 select 'portfolio', c.idea_seq, u.nickname, c.reg_dt, i.idea_nm, c.REPORT_TYPE, c.REPORT_REASON
  from db_portfolio.tb_idea_report c left join db_user.tb_user_info u
                                            on u.user_seq = c.user_seq
                                     left join db_portfolio.tb_idea i
                                             on c.idea_SEQ = i.IDEA_SEQ
 where c.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY)
union all
 select 'portfolio_comment', d.comment_seq, u.nickname, d.reg_dt, j.comment, d.REPORT_TYPE, d.REPORT_REASON
  from db_portfolio.tb_idea_comment_report d left join db_user.tb_user_info u
                                                    on u.user_seq = d.user_seq
                                             left join db_portfolio.tb_idea_comment j
                                                    on d.comment_SEQ = j.IDEA_SEQ
 where d.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY)
union all
 select 'community', e.comm_seq, u.nickname, e.reg_dt, k.TITLE, e.REPORT_TYPE, e.REPORT_REASON
  from db_community.tb_community_report e left join db_user.tb_user_info u
                                                 on u.user_seq = e.user_seq
                                          left join db_community.tb_community k
                                                 on e.COMM_SEQ = k.comm_seq
 where e.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY) 
union all
 select 'user', f.report_user_seq, u.nickname, f.reg_dt, f.user_seq, f.REPORT_TYPE, f.REPORT_REASON
  from db_user.tb_user_report f left join db_user.tb_user_info u
                                       on u.user_seq = f.report_user_seq
                                left join db_user.tb_user_info l
                                       on u.user_seq = f.user_seq
 where f.reg_dt > DATE_ADD(now(), INTERVAL -60 DAY);

select * from 최근2달신고;

이렇게 실행하면 아래와 같이 정리되어 나온다.

신고내역

프로그래머스 level 2 예상 대진표

|

걸린 시간 : 45분

문제 설명

△△ 게임대회가 개최되었습니다. 이 대회는 N명이 참가하고, 토너먼트 형식으로 진행됩니다. N명의 참가자는 각각 1부터 N번을 차례대로 배정받습니다. 그리고, 1번↔2번, 3번↔4번, … , N-1번↔N번의 참가자끼리 게임을 진행합니다. 각 게임에서 이긴 사람은 다음 라운드에 진출할 수 있습니다. 이때, 다음 라운드에 진출할 참가자의 번호는 다시 1번부터 N/2번을 차례대로 배정받습니다. 만약 1번↔2번 끼리 겨루는 게임에서 2번이 승리했다면 다음 라운드에서 1번을 부여받고, 3번↔4번에서 겨루는 게임에서 3번이 승리했다면 다음 라운드에서 2번을 부여받게 됩니다. 게임은 최종 한 명이 남을 때까지 진행됩니다.

이때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 궁금해졌습니다. 게임 참가자 수 N, 참가자 번호 A, 경쟁자 번호 B가 함수 solution의 매개변수로 주어질 때, 처음 라운드에서 A번을 가진 참가자는 경쟁자로 생각하는 B번 참가자와 몇 번째 라운드에서 만나는지 return 하는 solution 함수를 완성해 주세요. 단, A번 참가자와 B번 참가자는 서로 붙게 되기 전까지 항상 이긴다고 가정합니다.

제한사항

N : 21 이상 220 이하인 자연수 (2의 지수 승으로 주어지므로 부전승은 발생하지 않습니다.)
A, B : N 이하인 자연수 (단, A ≠ B 입니다.)

입출력 예

N A B answer
8 4 7 3

입출력 예 설명

첫 번째 라운드에서 4번 참가자는 3번 참가자와 붙게 되고, 7번 참가자는 8번 참가자와 붙게 됩니다. 항상 이긴다고 가정했으므로 4번 참가자는 다음 라운드에서 2번이 되고, 7번 참가자는 4번이 됩니다. 두 번째 라운드에서 2번은 1번과 붙게 되고, 4번은 3번과 붙게 됩니다. 항상 이긴다고 가정했으므로 2번은 다음 라운드에서 1번이 되고, 4번은 2번이 됩니다. 세 번째 라운드에서 1번과 2번으로 두 참가자가 붙게 되므로 3을 return 하면 됩니다.

사고 과정

tournament1 무슨 규칙이 있을까싶어서 그림을 그려보았다. 사실 이 때 깨달았어야 했다, 숫자가 계속 반띵되다가 같은 숫자가 되면 만나게 된다는 것을…

tournament1 ‘두 수가 8보다 작은 경우에는 3라운드(2의3승)안에서 만날 수 있지만 9부터 16까지의 수가 포함되어있는 경우에는 4라운드(2의4승)까지 가야 1부터 8사이의 참가자를 만날 수 있구나!’ 라고 생각해서 둘 중 큰 수를 루트한 값의 정수 부분이 답이라고 생각하고 아래와 같이 코드를 작성했는데 3개정도만 맞고 다 틀렸다.

import math

def solution(n,a,b):
    answer = 0
    if a > b: 
        a,b = b,a
    if int(b ** 0.5) == b ** 0.5:
        answer = int(b ** 0.5)
    else:
        answer = math.trunc(b ** 0.5) +1
    return answer

# solution(16,7,10)
solution(4,2,3) #2
solution(4,1,4) #2
solution(8,4,7) #3

왜냐하면 이렇게 구하면 최대 라운드수가 나오게 되는것이기 때문이다. 15,16번 참가자인 경우 1라운드에서 만나게 되는데 그런 부분이 반영되지 못한다.

숫자를 쪼개면서 두 숫자가 같아질 때까지 올라가는 횟수를 세어야한다.

예를 들어서 7번과 11번이 붙는다고 하면 한 라운드 후 7번은 4번이 되고 11번은 6번이 된다.
두 라운드 후 4번은 2번이 되고 6번은 3번이 된다.
세 라운드 후 2번은 1번이 되고, 3번은 2번이 된다.
네 라운드 후 1번은 그대로 1번, 2번은 1번이 되면서 a와 b가 같은 숫자가 되고
4번의 라운드를 거쳐서 만나게되는 것을 알 수 있다.

두번째 예를 들면 5번과 6번이 붙는경우, 5번이 3번이되고 6번이 3번이 되면서 a와 b가 3이 됨으로서 같은 숫자가 되고 1번의 라운드를 거쳐서 만나게되는 것을 알 수 있다.

제출 답안

def solution(n,a,b):
    answer = 0
    while a != b:
        answer += 1
        a, b = (a+1)//2, (b+1)//2
    return answer

모범 답안

def solution(n,a,b):
    return ((a-1)^(b-1)).bit_length()

번호 사이의 거리를 이진법의 XOR로 계산하는 신박한 풀이도 있었다.
자세한 풀이 내용은 아래 블로그에 잘 나와있다.

예상 대진표 문제 (3) - 비트연산과 이진트리 - 아트 & 코드

코딩테스트를 보다가 이 문제가 나와서 풀게되었는데 코딩테스트를 보면 긴장하면서 머리를 많이 쓰게 되고 문제를 풀지 못하게되면 아쉬워서 공부를 더 열심히 하게되는 것 같다.

프로그래머스 level 1 부족한 금액 계산하기

|

걸린 시간 : 30분

문제 설명

새로 생긴 놀이기구는 인기가 매우 많아 줄이 끊이질 않습니다. 이 놀이기구의 원래 이용료는 price원 인데, 놀이기구를 N 번 째 이용한다면 원래 이용료의 N배를 받기로 하였습니다. 즉, 처음 이용료가 100이었다면 2번째에는 200, 3번째에는 300으로 요금이 인상됩니다. 놀이기구를 count번 타게 되면 현재 자신이 가지고 있는 금액에서 얼마가 모자라는지를 return 하도록 solution 함수를 완성하세요. 단, 금액이 부족하지 않으면 0을 return 하세요.

제한사항

놀이기구의 이용료 price : 1 ≤ price ≤ 2,500, price는 자연수 처음 가지고 있던 금액 money : 1 ≤ money ≤ 1,000,000,000, money는 자연수 놀이기구의 이용 횟수 count : 1 ≤ count ≤ 2,500, count는 자연수

입출력 예

price money count result
3 20 4 10

입출력 예 설명

이용금액이 3인 놀이기구를 4번 타고 싶은 고객이 현재 가진 금액이 20이라면, 총 필요한 놀이기구의 이용 금액은 30 (= 3+6+9+12) 이 되어 10만큼 부족하므로 10을 return 합니다.

제출 코드

def solution(price, money, count):
    answer = 0
    total = 0
    for i in range(1, count+1):
        total += price*i
    answer = total- money
    if answer < 0:
        return 0
    return answer

모범 답안

def solution(price, money, count):
    return max(0,price*(count+1)*count//2-money)

3,6,9 를 수열이라보고 등차수열의 합으로 비용을 구한다.

count는 자연수만 주어지므로 count와 count+1 둘 중 하나는 무조건 짝수입니다. 따라서 이 문제에서는 //말고 /를 써도 아무런 지장이 없습니다. -KJS

프로그래머스 level 1 두 정수 사이의 합

|

문제 설명

두 정수 a, b가 주어졌을 때 a와 b 사이에 속한 모든 정수의 합을 리턴하는 함수, solution을 완성하세요. 예를 들어 a = 3, b = 5인 경우, 3 + 4 + 5 = 12이므로 12를 리턴합니다.

제한 조건

a와 b가 같은 경우는 둘 중 아무 수나 리턴하세요. a와 b는 -10,000,000 이상 10,000,000 이하인 정수입니다. a와 b의 대소관계는 정해져있지 않습니다.

입출력 예

a b return
3 5 12
3 3 3
5 3 12

제출 코드

def solution(a, b):
    answer = 0
    for i in range(min(a,b),max(a,b)+1):
        answer += i
    return answer

모범 답안

def adder(a, b):
    if a > b: a, b = b, a
    #a가 b보다 크면 a,b 의 위치를 바꾼다
    return sum(range(a,b+1))

min, max를 하면 리스트의 모든 원소를 한번씩 돌기때문에 시간복잡도가 O(n)이어서 이렇게 하는게 훨씬 빠를 것 같다. 파이썬 기본함수들의 시간복잡도

프로그래머스 level 1 2016년

|

걸린 시간 : 1시간

문제 설명

2016년 1월 1일은 금요일입니다. 2016년 a월 b일은 무슨 요일일까요? 두 수 a ,b를 입력받아 2016년 a월 b일이 무슨 요일인지 리턴하는 함수, solution을 완성하세요. 요일의 이름은 일요일부터 토요일까지 각각 SUN,MON,TUE,WED,THU,FRI,SAT

입니다. 예를 들어 a=5, b=24라면 5월 24일은 화요일이므로 문자열 “TUE”를 반환하세요.

제한 조건

2016년은 윤년입니다.
2016년 a월 b일은 실제로 있는 날입니다. (13월 26일이나 2월 45일같은 날짜는 주어지지 않습니다)

입출력 예

a b result
5 24 "TUE"

제출 코드

def solution(a, b):
    day_dic = {"1":"FRI", "2":"SAT", "3":"SUN", "4":"MON", "5":"TUE", "6":"WED", "0":"THU"}
    #2016년 1월 1일이 금요일이니까
    daylist = [31,29,31,30,31,30,31,31,30,31,30,31]
    #윤년이라 2월은 29일까지
    day=0
    
    if a != 0:
        for m in range(a-1):
            day += daylist[m]
    
    day = day + b
    answer = str(day%7)

    for key, value in day_dic.items():
        answer = answer.replace(key, value)
    return answer
    #숫자를 문자로 바꾸는 부분은 저번에 풀었던 '숫자 문자열과 영단어'에서 가져왔다

키밸류 replace는 문자열만 되는 모양이다.

String.prototype.replace() : replace() 메서드는 어떤 패턴에 일치하는 일부 또는 모든 부분이 교체된 새로운 문자열을 반환합니다. 그 패턴은 문자열이나 정규식(RegExp)이 될 수 있으며, 교체 문자열은 문자열이나 모든 매치에 대해서 호출된 함수일 수 있습니다. - MDN

모범 답안

def getDayName(a,b):
    months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    days = ['FRI', 'SAT', 'SUN', 'MON', 'TUE', 'WED', 'THU']
    return days[(sum(months[:a-1])+b-1)%7]

sum(months[:a-1]) 아아주 간단명료하다

손가락 코딩

프로그래머스에 이런 코드가 박제되어있었다…. ㅋㅋㅋ

def getDayName(a,b):
    answer = ""
    if a>=2:
        b+=31
        if a>=3:
            b+=29#2월
            if a>=4:
                b+=31#3월
                if a>=5:
                    b+=30#4월
                    if a>=6:
                        b+=31#5월
                        if a>=7:
                            b+=30#6월
                            if a>=8:
                                b+=31#7월
                                if a>=9:
                                    b+=31#8월
                                    if a>=10:
                                        b+=30#9월
                                        if a>=11:
                                            b+=31#10월
                                            if a==12:
                                                b+=30#11월
    b=b%7

    if b==1:answer="FRI"
    elif b==2:answer="SAT" 
    elif b==3:answer="SUN"
    elif b==4:answer="MON"
    elif b==5:answer="TUE"
    elif b==6:answer="WED"
    else:answer="THU"
    return answer


print(getDayName(5,24))

내장 함수 코딩

from datetime import date
DAY = {0: 'MON', 1: 'TUE', 2: 'WED', 3: 'THU', 4: 'FRI', 5: 'SAT', 6: 'SUN'}

def getDayName(a,b):
    d = date(2016, a, b)
    return DAY[d.weekday()]

빠른 길을 좋아하는 사람들을 위한 내장함수 코딩도 있다.

프로젝트오일러 19번 20세기에서, 매월 1일이 일요일인 경우는 몇 번?

|

문제 설명

다음은 달력에 관한 몇 가지 일반적인 정보입니다 (필요한 경우 좀 더 연구를 해 보셔도 좋습니다).

1900년 1월 1일은 월요일이다.
4월, 6월, 9월, 11월은 30일까지 있고, 1월, 3월, 5월, 7월, 8월, 10월, 12월은 31일까지 있다.
2월은 28일이지만, 윤년에는 29일까지 있다.
윤년은 연도를 4로 나누어 떨어지는 해를 말한다. 하지만 400으로 나누어 떨어지지 않는 매 100년째는 윤년이 아니며, 400으로 나누어 떨어지면 윤년이다.
20세기 (1901년 1월 1일 ~ 2000년 12월 31일) 에서, 매월 1일이 일요일인 경우는 총 몇 번입니까?

제출 답안

daylist = [31,28,31,30,31,30,31,31,30,31,30,31]
total=0
count=0

for y in range(1900,2001):
#1900년부터 2001년까지
    for m in range(12):
    #1900년부터 2001년까지 중 1월(index0)부터 12달까지
        day=daylist[m]
        #해당 달의 일수를 day에 저장
        if y%4==0 and m==1: 
        #윤년이고 2월(index1)이면
            day+=1
            #하루 더한다
        for d in range(day):
        #1900년부터 2001년까지 중 1월부터 12달까지 중 월에 해당하는 날들 동안
            if y>1900 and d==0 and total%7==6:
            #1901년이고 첫번째 날이고 일요일 경우에
                count+=1
                #카운트를 1더한다
            total+=1
            #1900년부터 2001년까지 일수계산
print(total,count)
#36891 171
#전체 일수, 첫째날이면서 일요일인 날의 수

인터넷에 그런 풀이도 있었다.
1일이 100년동안 1200번 있을건데 그럼 1200을 7로 나누면 171.4이므로 일요일을 비롯한 각 요일들이 171번 있을 것이다.
역시 사람은 머리를 열심히 써야한다.

위코드 수료 후 백엔드 면접 후기 및 FAQ2 - 인성면접

|

온보딩이 종료되고 취준을 다시 시작했는데 이번에는 신중하게 붙을 가능성이 있는 곳 위주로 지원했다. 트릿지에서는 코딩테스트를 통과했는데 컬처핏에서 떨어져서 약간 의아했고 크립토파라다이스의 코딩테스트는 너무 어려워서 풀지 못했다. 브랜디 코딩테스트는 3개 중에 1개만 맞았다.

전에 붙었던 회사에서 또 전화가 왔는데 1달만에 개발 전체 담당자분이 바뀌었어서 안 가길 잘했다는 생각이 들었다. 잡플래닛 평점이 2점대인 회사에 면접을 보러갔었는데 회사는 괜찮아보였지만 개발이 메인이 아니고 운영 직군의 입김이 센 것 같았다.

원티드 4개, 링크드인 1개, 로켓펀치에 30개 정도를 넣었고 면접을 6개, 코딩테스트를 3개 정도 보았다. 인성면접과 컬쳐핏면접을 몇번 보게되었는데 주로 개발자로서의 장/단점과 프로젝트를 하면서 어려웠던 점과 그 이유를 많이 물어보아서 정리해보았다.

개발자로서의 장점과 단점

장점

항상 지식을 공유하고 좋은 커뮤니케이션을 통해서 문제 해결을 위해 함께 힘쓴다.
프론트엔드 개발에 대한 지식도 겸비하고 있어 협업시 소통을 원활하게 할 수 있다. (예리님께서 이건 단점으로 보일 수도 있다고 하셨다.)

어려운 기능을 구현하는 와중에도 끈기를 가지고 긍정적인 마인드를 가졌다. 모르는 부분에 대해서 질문하는 것을 두려워하지 않으며 이해도도 높아 학습의 속도가 빠르다.적극적인 의사표시로 자신의 의견을 전달할 줄 알고, 개발 의외에 부분에서도 인간적으로 상대방을 챙기고 돕는다.

단점

스페셜리스트보다는 제네럴리스트인 성격이라 에너지가 분산되는 경향이 있다. 예를 들어서 독일어(B1), 스페인어(IM3), 일본어를 조금씩 하는데 이 시간과 노력을 전부 영어에 쏟았다면 영어로 더 글을 잘 쓸 수 있었을거라고 생각한다. 같은 맥락으로 웹디자이너로 일하지 않고 바로 백엔드 개발 공부를 시작했었더라면 지금 더 잘 할 수 있었을 것 같지만 이제라도 다른 것 외에 백엔드 개발만 생각하려고 한다.

실력이 부족해서 자신감이 부족한 경향이 있다. 공부를 많이 해서 실력이 늘면 나아지지 않을까 생각한다.

프로젝트 하면서 어려웠던 점

1차 프로젝트때 PM을 맡았는데 잘 진행을 하다가 어떤 기능이 구현이 안 되서(회원, 비회원 토큰관련) 여유가 없어서 팀 전체를 잘 챙기지 못했는데 다행히 정말 좋은 팀원분들을 만나게되어서 다른 팀원분들께서 함께 챙겨주셔서 잘 마무리했다.

2차 프로젝트때는 맡은 기능 + AWS 구현에 집중했는데 새로운 인프라 관련 기능을 구현하느라 즐거워서 지하철에서도 코딩을 할만큼 열심히 했다. 팀분위기는 아주 좋았고 원하는 기능도 모두 잘 구현했지만 AWS 객체를 삭제하는 부분이 원활하지 못했어서 끝나고 인턴을 하면서 공부하고 적용했다.

인턴(5주, 1프로젝트)을 할 때는 팀 분위기가 굉장히 좋았고 채용 관리자페이지를 마지막까지 잘 구현했다. API정리를 막판에 하다가 시간에 쫓기긴했는데 결국 원하는 기능은 전부 구현했고 기획대로 잘 돌아가고 팀원들과도 돈독하고 좋았다.

온보딩때는 내가 제일 못하는 팀원이었는데 위코드와 다르게 서로 모르는 상태로 어려운 프로젝트(5주, 7개 프로젝트)를 타이트한 스케줄로 들어가서 이틀에 한 개씩 프로젝트를 하면서 매번 밤을 새고 압박, 좌절, 체력의 한계를 느끼게되었다. 실력이 부족하면 개발자로 함께 일할 수 없겠다는 생각이 들어서 조금 현타가 왔었던 것 같다. 추후 온보딩 프로젝트의 난이도가 굉장히 높은 것이었다는 이야기를 듣고 조금 안심이 되었다.

온보딩 후 동료 평가 내용

긍정적 동료되기

항상 긍정적인 성격을 가지고 계시고, 어느 상황에서나 침착함을 잃지 않으십니다. 덕분에 조금 편한 마음으로 프로젝트를 진행했습니다. 모여서 과제를 할 때 답답한 분위기를 환기시켜주셔서 감사했습니다.

도전하고 주도하기

모르는 기술인 DRF를 바쁜일정에도 습득 하시려고 노력을 하셨습니다. 미리 결과를 단정짓지 않고 도전하시는 부분들을 많이 가지고 계셔서 저에게 많이 도전이 되었습니다. 팀의 리더가 있고 선후배 개발자가 아닌 같은 레벨에 있는 팀원들끼리 프로젝트를 하는데 있어서 팀원들의 의견을 잘 들어주시고 배려와 수용하시는 면이 많으셨습니다. 조금만 더 본인의 의견이나 목소리를 내어 주시면 완벽할거 같습니다. 밤까지 함께 과제를 수행하며 끝까지 해결할 수 있어서 좋았습니다.

린하게 해결하기

직접 맡으신 부분을 바로 수행하시는 모습이 인상적 이었습니다. 구현 -> 테스트 단계별로 구현하셨고, 테스트 구현시 실패를 하게되면 원인을 찾아서 코드를 수정하셨습니다. 전체적인 흐름을 이해한 뒤 프로젝트를 진행해주시고, 본인이 할 수 있는 범위에 대해서 먼저 공유해주셔서 좋았습니다.

데이터로 소통하기

여러 사이트와 문서를 참고하여 기능 구현하시는 점이 인상깊었습니다. 다양한 데이터에 대한 정보를 공유하며 문제해결에 힘써주셨습니다. 일례로 url설계나 db설계를 외부 소스에서 참고해주시는 면이 좋았습니다. 과제마다 접해보는 기술이 다르고 새로웠기 때문에, 관련 자료를 찾아서 공유해주셨습니다.

집단지성 활용하기

모르시거나 의문의 드는점들을 계속 공유해주셔서 구현하시는 부분을 어느정도 하신건지 잘 알 수 있었습니다. 공부할 수 있는 자료나 새롭게 알게 된 부분들은 팀 채널에 먼저 공유해주시며, 팀이 좋은 방향으로 나아갈 수 있게끔 해주셨습니다. 여러 방안에 대해 피드백 주시고 함께 얘기하며 구현을 할 수 있었던 것 같습니다.팀원 전체의 의견을 잘 수용해주셨습니다.

품질과 기한 지키기

함께 새로운 약속을 할때 빠짐없이 잘 참여해주셨습니다. 상황에 따라 구현의 순서나 진행 순서를 변경하여 효율적으로 진행할 수 있었던 것 같습니다.

동료가 앞으로 더 성장하기 위해 노력하면 좋을 부분이 있다면 알려주세요.

팀원들의 이야기를 잘 들어주시는 만큼 본인의 의견도 많이많이 이야기해주시면 더욱 좋을것 같습니다.! 백엔드 부분을 배우신지 얼마 되지 않으셨는데 다양한 기능을 구현하시는 것을 보며 대단하다고 생각했습니다. 이번에 프로젝트를 진행하시면서 접해보신 기술들(Docker, DRF)을 연습하셔서 어느정도 할 수 있을 정도로 익히시면 좋을 것 같습니다. 개발실력도 있으시고, 문제를 풀어가는 힘도 가지고 계신데 자신감이 없으신 것 같아서 조금 속상했습니다. 자신감만 가지시면 분명히 좋은 개발자가 되실 거라고 확신합니다. 같이 프로젝트를 하게 되어 좋았습니다.

동료에게 특별히 응원/ 칭찬할 말이 있다면 알려주세요.

이전에 경력과 부트캠프 경험으로 봤을때는 더 퍼포먼스를 내실 수 있으실것 같은데, 먼가 환경이 잘 안맞아서 상대적으로 퍼포먼스가 안 나신것 처럼 보였습니다. 하지만 앞에서 말했듯이 이전 경험이 충분하므로, 시간과 여건만 된다면 퍼포먼스를 내실것으로 생각됩니다. 고생많으셨습니다!

루나님이 계셔서 프리온보딩 기간이 너무 좋았고, 의지가 되었습니다. 그리고 공부자료를 계속 공유해주시는 모습들이 너무 인상적이었고 감사했습니다.