루나의 TIL 기술 블로그

코데카데미 리액트 기초과정 요약 - props

|

props

pros는 properties의 줄임말로 생각하면 된다.

코데카데미 리액트 기초과정 요약 - 리액트의 기본구조

|

리액트 기본 앱 구조

App.js

import React from 'react';

function MyComponent() {
  return <h1>Hello world</h1>;
}

export default MyComponent;

index.js

import React from 'react';

function MyComponent() {
  return <h1>Hello world</h1>;
}

export default MyComponent;

index.html

import React from 'react';

function MyComponent() {
  return <h1>Hello world</h1>;
}

export default MyComponent;

리액트 컴포넌트

  • React는 컴포넌트로 이루어져있다. 컴포넌트에서 UI 조각들을 렌더한다.
  • 렌더하기 위해서는 react와 reactDOM를 import해야한다.
  • 컴포넌트들은 자바스크립트 함수에 의해 정의된다.
  • 함수 컴포넌트 이름은 대문자로 시작해야하고 파스칼케이스(CamelCase)로 쓰여져야한다.
  • 함수 컴포넌트는 JSX 문법에 따라 리액트 요소들을 반환해야만한다.
  • 리액트 컴포넌트들은 파일에서 파일로 export, import될 수 있다.
  • 리액트 컴포넌트들은 HTML같이 생긴 ‘</>’ 문법으로도 불러와질 수 있다.
  • 리액트 컴포넌트를 렌더하려면 root 컨테이너를 명시하기 위해 .createRoot()가 있어야하고 .render()로 불러와야한다.

리액트는 프레임워크일까 라이브러리일까?

프레임워크와 라이브러리는 둘 다 코드 작성에 도움이 되는 타인이 작성한 코드의 집합이다.

프레임워크는 사용방법이 정해져 있어서 이에 따라서 개발자가 개발해나가면 되는 것이고 라이브러리는 개발자가 필요할 때마다 가져다가 설치, 호출하여 사용할 수 있는 것이다.

제어권이 어느쪽에 있느냐에 따라 구분할 수 있고 리액트는 그리하여 라이브러리라고 볼 수 있다.

포트폴리오 리스트 유닛 디자인 업데이트

|

포트폴리오 리스트 유닛 바꾸기 전

바꾸기 전

포트폴리오 리스트 유닛 바꾼 뒤 (최종디자인)

바꾼 뒤

회고

유저에게 시각적 즐거움을 제공하고 흥미롭게 여길만한 투자정보를 미리 보여주고싶어서 시작한 기획이었는데 와이어프레임을 그리는 것은 금방 했는데 설득의 시간이 굉장히 오래 걸렸고 의사소통 과정이 부정적이었어서 이직을 생각하는 계기가 되었다. 빌드되는 과정에서 여러가지 정보가 추가되어서 유닛이 커지게 되었지만 이전보다 확실히 보기좋아서 뿌듯했다.

스케치

첫 스케치 첫 스케치

피그마 피그마

디자인 디자인

사후평가

몽고DB와 SQL에서 주차별 포트폴리오 생성수, 포트폴리오 GNB 방문수를 확인한 결과 1.5배가량의 트래픽이 증가한 것을 확인할 수 있었다.

주차 포트폴리오 생성수 포트폴리오 방문수(GNB) 상세 클릭 빈도
-1주차 102 566 396
1주차 120 803 247
2주차 207 849 329
3주차 309 974 457

추출에 사용한 쿼리

처음에는 값이 맞게 나오지 않아서 내부 개발 직원들의 아이디를 조회 대상에서 제외했더니 값이 맞게 나왔다. 몽고디비에는 날짜가 초형태로 저장되어있어서 sql에서 바꾸어서 집어넣었다.

sql에서 date를 timestamp로 변환

select unix_timestamp('2022-09-26')

sql

select count(1) 
from db_portfolio.tb_idea a left join db_user.tb_user_info b 
		  on a.user_seq = b.user_seq
where REG_DT > '2022-10-17' and REG_DT < '2022-10-24' 
  and a.user_seq not in (73,385,8919,5035, 5015, 7516)

mongoDB

db.menu_view.find({'menu':'portfolio', 'reg_ts' : {$gte:1666537200, $lte:1667142000},'user_seq':{$nin:[72,73,385,5015,5035,7516,8919]}}).count()

커밋 컨벤션 예시

|

목적

여러명이 커밋을 남길 때, 전에 작업했던 내용을 찾아보기 쉽게, 코드에 마우스오버했을 때 어떤 작업이었는지 설명을 볼 수 있게하기 위해서 커밋을 잘 쓰도록 장려하려고한다.

브랜치 이름 짓기 규칙

“브랜치이름” + “/” + “기능범위” + “-” + “설명(명사,동사 필요한대로)”

브랜치 이름

feat : 기능 추가
update : 기능 업데이트 fix : 버그 및 에러 수정 hotfix : 운영에서 바로 에러 수정 add : 스케줄 파일 등 추가
refactor : 코드 리팩토링

브랜치 이름 예시

feat/referral 친구추천 추가
feat/push_update 푸시세팅 업데이트
feat/identity_verify 신원조회 추가
fix/board_summary 게시글 서머리 에러 수정
hotfix/board_related 관련 게시글 에러 수정(운영)
add/notice_redis 공지글 redis에 추가 refactor/board_video 영상게시글 관련코드 리팩토링

커밋 컨벤션

이슈번호 타입(범위): 설명

타입

add: 새로운 기능 추가
update: 기능 부분 업데이트
fix: 버그 수정
docs: 문서 수정
chore: 빌드 스크립트 설정 변경, 패키지 매니저 수정
test: 테스트 코드, 리팩토링 테스트 코드 추가
refactor: 코드 리팩토링
ci: ci 관련 스크립트 파일 수정
merge: merge 시 사용

예시

#65 add(board_temp): 임시저장 추가
#34 update(i2e): 프리미엄스코어 로직 수정 #66 fix(community) : 커뮤니티 리스트 오류 수정 #72 docs(convention) : 커밋 컨벤션

Redis 사용 예시

|

목적

레디스에 키밸류 형태로 데이터를 저장하면 메모리에 올라가서 디비에서 매번 쿼리 실행을 통해서 가져오는 것보다 더 빠르게 데이터를 가져올 수 있다. 특히 변하지 않는 값을 가져오려고 할 때 사용한다.

기본 명령어

redis-cli -h 12.345.678.99 -p 6379

앱의 실행파일이 있는 위치에서 ip주소와 포트번호를 입력한다.

select 2 # db번호
keys * # 2번 db에 들어있는 키를 모두 보여준다

set notice 24926 #키, 밸류 설정, 주어진 키에 하나의 값만 저장
"OK"
get notice #키로 밸류값 찾기
"24926"
SET anotherkey "will expire in a minute" EX 60 #1분 뒤 삭제되도록 설정
"OK"

잘 정리되어있는 블로그 : https://sjh836.tistory.com/178

사용 예시

특정 게시글을 블락하고 싶을 때


class RedisConfig:
    config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
    config = configparser.ConfigParser()
    config.read(config_path)

    server_name = config['DEFAULT']['SERVER_NAME']

    redis_intra = connect_redis_user(server_name)
    redis_info = config[redis_intra]

    REDIS_INFO = {
        'block_board': {
            "host": redis_info['host'], "port": redis_info['port'], "db": 12 # 차단 게시글
        }
    }


from configuration import redis_block_board

block_boards = redis_block_board.keys("*")

 if block_boards:
            block_boards_list = str(block_boards).strip('[,]')
            for block_board in block_boards:
                read_sql = f""" select user_seq from db_community.tb_board where board_seq = {block_board}"""
                cursor.execute(read_sql)
                row = cursor.fetchone()
                if user_seq != row['user_seq']:
                    sql += f""" and b.board_seq != {block_board}"""

쿼리디버거, Lazy Loading, Eager Loading

|

목적

쿼리의 로딩이 느리다고 판단되는 경우에 빠르게 만들어주기 위해서 쿼리를 실행해서 관련 데이터들을 미리 가져와놓고 거기서 필요한 데이터를 출력해주는 방법이 있다. 관련 코드를 공유한다. 코드의 출처는 위코드에 있다.

쿼리 실행 시간을 측정해주는 쿼리디버거 데코레이터

import functools, time
from django.db   import connection, reset_queries
from django.conf import settings


def query_debugger(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        reset_queries()
        number_of_start_queries = len(connection.queries)
        start  = time.perf_counter()
        result = func(*args, **kwargs)
        end    = time.perf_counter()
        number_of_end_queries = len(connection.queries) - 2 if len(connection.queries) else 0
        print(f"-------------------------------------------------------------------")
        print(f"Function : {func.__name__}")
        print(f"Number of Queries : {number_of_end_queries-number_of_start_queries}")
        print(f"Finished in : {(end - start):.2f}s")
        print(f"-------------------------------------------------------------------")
        return result
    return wrapper

쿼리 실행시간을 줄이기 위한 다양한 방법들


from django.views           import View
from django.http            import JsonResponse
from django.db.models       import Prefetch, Count
from django.db.models.query import QuerySet

from .models      import Book, Store, Publisher
from decorators   import query_debugger

#################################
# Lazy Loading (지연 로딩)
#################################
class LazyLoadingCheckView(View):
    @query_debugger
    def get(self, request):
        # Lazy Loading - queryset1, queryset2, queryset3는 즉시 호출되지 않음.
        queryset = Publisher.objects.all()
        queryset2 = queryset.exclude(id=2).annotate(count=Count('book'))

        # Lazy Loading시 쿼리는 어디에 저장되어 있는가?
        print("queryset.query에 저장된 SQL문 :: ", queryset.query)
        print("queryset2.query에 저장되 SQL문 :: ", queryset2.query)
#
        # Queryset Evaluation - 실제로 db를 호출하는 시점 : Slicing, Iteration, repr(), len(), list(), bool() ..
        # Example 1. list(queryset3)
#        list(queryset)

        # Example 2. Iteration
#        for i in queryset2:
#            print(i.name)
#
        # Lazy Loading 때문에 발생하는 문제 - 매번 DB에 요청을 보낸다.
#        list(queryset2)
        list(queryset2)
        queryset2[0]
        queryset2[0]
        queryset2[0]
#
        return JsonResponse({'message' : 'SUCCESS' }, status=200)



#############################
# Caching
#############################
class CachingCheckView(View):
    @query_debugger
    def get(self, request):
        queryset = Publisher.objects.all().annotate(count=Count('book'))


        # queryset이 평가될 때, data가 caching되지 않는 경우
#        print("before queryset._result_cache :: ", queryset._result_cache)
#        queryset[0]
#        print("after queryset._result_cache :: ", queryset._result_cache)
#        queryset[0]
#        print("after queryset._result_cache :: ", queryset._result_cache)
#        queryset[0]

        # queryset이 평가될 때, data가 caching 되는 경우        
        # queryset이 평가될 때, 값을 QuerySet._result_cache에 저장한다.
        print("before queryset._result_cache :: ", queryset._result_cache)
        for publisher in queryset:
            a = publisher.id
        print("after queryset._result_cache :: ", queryset._result_cache)
        
        queryset[0]
        queryset[0]
        queryset[0]
        queryset[0]

        return JsonResponse({'message' : 'SUCCESS' }, status=200)


#############################
# N + 1 Problems
#############################
class BooksWithAllMethodView(View):
    @query_debugger
    def get(self, request):
        print('Book에서 Publisher Instance에 접근하는 경우 <정참조>')
        # Publisher(One) - Book(Many)

        queryset = Book.objects.all()
        books    = []

        # QuerySet이 평가(Evaluation)될 때, N + 1 Problems 발생
        # 모든 book을 조회하는 SQL 1번 실행
        # book 하나당 publisher를 매번 조회하는 SQL N번 실행
        for book in queryset:
            books.append({
                'id': book.id,
                'name': book.name,
                'publisher': book.publisher.name # book.publisher에 접근, 캐싱되지 않은 데이터이므로 query 발생
                }
            )

        """
        SELECT * FROM books ; 1번
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 20
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 21
        """

        return JsonResponse({'books_with_all_method' : books }, status=200)


########################################
# Eager Loading (select_related)
#######################################
class BooksWithSelectRelatedView(View):
    @query_debugger
    def get(self, request):
        queryset = Book.objects.all().select_related("publisher")
        print("queryset.query에 저장된 SQL문 :: ", queryset.query)

        books = []

        for book in queryset:
            books.append({
                'id': book.id,
                'name': book.name,
                'publisher': book.publisher.name
                }
            )

        return JsonResponse({'books_with_all_method' : books }, status=200)



#############################
# N + 1 Problems
#############################
class StoresWithAllMethodView(View):
    @query_debugger
    def get(self, request):
        print(f'Store에서 Book Instance에 접근하는 경우 <역참조>')
        queryset = Store.objects.all()
        stores   = []

        for store in queryset:
            books = [book.name for book in store.books.all()]
            stores.append({
                'id': store.id,
                'name': store.name,
                'books': books
                }
            )
        """
        SELECT * FROM store ; 1번
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        SELECT `publishers`.`id`, `publishers`.`name` FROM `publishers` WHERE `publishers`.`id` = 19
        """

        return JsonResponse({'stores_with_all_method' : stores }, status=200)


########################################
# Eager Loading (prefetch_related)
#######################################
class StoresWithPrefetchRelatedView(View):
    @query_debugger
    def get(self, request):
        queryset = Store.objects.all().prefetch_related("books")
        print("queryset.query에 저장된 SQL문 :: ", queryset.query)
        print("final after queryset._result_cache :: ", queryset._result_cache)
        print("final after queryset._prefetch_related_lookups :: ", queryset._prefetch_related_lookups)

        stores = []

        for store in queryset:
            books = [book.name for book in store.books.all()]
            stores.append({
                'id': store.id,
                'name': store.name,
                'books': books
            })

        print("!!!! result_cache :: ", queryset._result_cache)

#        stores2 = []
#
#        for store in queryset:
#            books = [book.name for book in store.books.all()]
#            stores2.append({'id': store.id, 'name': store.name, 'books': books})
#
        return JsonResponse({'stores_with_prefetch_related' : stores }, status=200)


##################################################
# Eager Loading (prefetch_related ) when filtering
##################################################
class StoresWithPrefetchNoneObjectView(View):
    @query_debugger
    def get(self, request):
        queryset = Store.objects.all().prefetch_related("books")

        stores = []

        for store in queryset:
            total_books    = [book.name for book in store.books.all()]
            filtered_books = [book.name for book in store.books.filter(name='Book9991')]
            stores.append({
                'id'          : store.id,
                'name'        : store.name,
                'total_books' : total_books,
                'filterd_books' : filtered_books
            })

        return JsonResponse({'stores_with_prefetch_related' : stores }, status=200)


##################################################
# Eager Loading (prefetch_related ) when filtering
##################################################
class StoresWithPrefetchObjectView(View):
    @query_debugger
    def get(self, request):
        queryset = Store.objects.prefetch_related(
            Prefetch('books', queryset=Book.objects.all(), to_attr='total_books'),
            Prefetch('books', queryset=Book.objects.filter(name='Book9991'), to_attr='filtered_books'),
        )

        print("queryset.query에 저장된 SQL문 :: ", queryset.query)
        print("final after queryset._result_cache :: ", queryset._result_cache)
        print("final after queryset._prefetch_related_lookups :: ", queryset._prefetch_related_lookups)

        stores = []

        for store in queryset:
            total_books    = [book.name for book in store.total_books]
            filtered_books = [book.name for book in store.filtered_books]
            stores.append({
                'id'          : store.id,
                'name'        : store.name,
                'total_books' : total_books,
                'filterd_books' : filtered_books
            })

        return JsonResponse({'stores_with_prefetch_related' : stores }, status=200)

몽고db 기본 명령어 사용법

|

목적

몽고디비는 nosql데이터베이스로 데이터를 json형태로 빠르게 저장하고 불러오기 위해서 사용한다.
노마드코더에서 니꼬가 SQL 과 NoSQL은 한식과 노한식이라고 생각하면 좋다고 했는데 그게 제일 쉽고 직관적인 설명인 것 같다.

기존에 가장 익숙한 DBMS(Database Management System)은 SQL과 Oracle이 있는데 둘 다 관계형 데이터베이스 RDMS(Relation Database Management System)를 지원한다. 테이블마다 pk를 이용해서 넣어져있는 데이터들의 관계적 형태를 이용해서 조인과같은 연산을 통해 원하는 데이터를 조작할 수 있다.

NoSQL(Non Relational Operation Database SQL)의 줄임말로 관계형이 아닌 데이터베이스라는 뜻이다.
일반 SQL에서는 join을 하면서 성능 저하가 있을 수 있는데 MongoDB는 json형태로 데이터를 저장하고 서로 간의 관계가 없기때문에 더 빠르게 데이터를 불러올 수 있다.

NoSQL의 장점

1) 불필요한 Join의 최소화
2) 유연성있는 서버 구조 제공
3) 비정형 데이터 구조로 설계비용 감소
4) Read/Write가 빠르며 빅데이터 처리가 가능
5) 저렴한 비용으로 분산처리 및 병렬처리 가능

4번 장점의 경우는 반드시는 아니고 일반적인 관계형 데이터베이스가 빠른 경우도 많다. 그리고 비정형 데이터로 인해  관계형 데이터베이스보다 1.5배정도 용량을 많이 차지한다.

이러한 NoSQL은 크게 4종류의 모델이 있고 대표적인 데이터베이스는 아래와 같다.

1) KEY-VALUE - Redis , Memcached
2) COLUMN - Hbase, Casandra
3) DOCUMENT - MongoDB, 
4) GRAPH - GraphDB
출처: https://cionman.tistory.com/44 [Suwoni블로그:티스토리]

관계형 데이터베이스(Relational Database)와 MongoDB 논리적 구조 비교

Relational Database MongoDB
Table Collection
Row Document
Column Field
Primary Key Object_ID Field
Relationship Embbeded & Link

기본 명령어

mongosh {ip주소} -u {아이디} -p {비밀번호}

show dbs
use user
db.stats()
show collections
db.user_active.find()
db.user_active.find({'active_dt':{$gt:'2022-04-01',$lt:'2022-04-08'}})

show dbs
use search
db.stats()
show collections
db.company.find()
db.company.find({'symbol':'018260.KS'})
db.company.find({'reg_dt':{$gt:'2022-07-01',$lt:'2022-07-22'}})

json이니까 키값이 문자열이라 따옴표를 붙여주어야한다.

예시

{
    _id: ObjectId("63100d42c6df6b34c27c61d4"),
    menu: 'mypage',
    user_seq: 1,
    reg_ts: 1661996354
  },
  {
    _id: ObjectId("63100d44224bf614208dc6f7"),
    menu: 'portfolio',
    user_seq: 1,
    reg_ts: 1661996356
  },
  {
    _id: ObjectId("63100d45c6df6b34c27c61d5"),
    menu: 'feed',
    user_seq: 1,
    reg_ts: 1661996357
  }

db.menu_view.find({'menu':'portfolio', 'reg_ts' : {$gte:1664982000, $lte:1665586800}}).count()

#10월17일부터 24일까지 리스트 안의 유저가 아닌 유저들 중 메뉴가 포트폴리오인 것의 갯수 
db.menu_view.find({'menu':'portfolio', 'reg_ts' : {$gte:1665932400, $lte:1666537200},'user_seq':{$nin:[72,73,385,5015,5035,7516,8919]}}).count()

파이썬에서 사용할 때

공개되지 않은 config.ini 파일에 디비 정보를 넣어놓고 끌어와서 사용한다.

#config > config.ini
[TEST_DOCUMENT_DB]
HOST = 15.123.456.78
PORT = 12345
ID = lunayyko
PASSWORD = claire1234
#config > config_doc_db.py
import os
import configparser
from config.direction import connect_doc_db

config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
config = configparser.ConfigParser()
config.read(config_path)
server_name = config['DEFAULT']['SERVER_NAME']


class DocumentDB:
    config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
    config = configparser.ConfigParser()
    config.read(config_path)

    doc_db = connect_doc_db(server_name)
    doc_db_info = config[doc_db]

    DOC_DB_INFO = {
        "host": doc_db_info['HOST'],
        "port": int(doc_db_info['PORT']),
        "username": doc_db_info["ID"],
        "password": doc_db_info["PASSWORD"]
    }
#api 파일
from config.config_doc_db import DocumentDB as con_DocDB

def apifile(conn_mongo):
    doc_db = conn_mongo.user # db이름 user
    success = save_doc_db(db='community', table='board', key='board_seq', args=result, method='POST')
    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
    elif 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_mongo = MongoClient(**con_DocDB.DOC_DB_INFO, **{'retryWrites': False})

pandas 데이터프레임

|

목적

판다스는 데이터 조작과 분석을 위한 파이썬 소프트웨어 라이브러리이다. 패널데이터에서 유래했다. 같은 데이터를 여러 객체로 만들어서 수정, 삭제 등 여러 데이터 조작을 해볼 수 있고 메모리에만 올라가서 속도가 빠르다.

데이터프레임 예시

name math english
0 1 2
이서연 김민준 박지우
70 80 90
80 80 30

열 1개 추출하기

#시리즈 형태
df['english']

결과값 :

#데이터프레임 형태
df[['english']]

결과값 :

행 1개 추출하기

loc 함수를 이용해서는 조건 인덱싱이 가능합니다. index와 column 이름 대신 인덱싱을 원하는 조건을 loc 함수 내에 적용해주면 된다. 예를 들어, 수학 점수가 90점 이상인 학생들의 목록을 찾고 싶을 때는 다음과 같이 해주면 된다.

df.loc[]
df.loc[df['math'] >= 80]

참조: https://jimmy-ai.tistory.com/226

데이터프레임 비교시 and, or 사용

참조: https://mindscale.kr/course/pandas-basic/bool-selection/

판다스 기초 위키독스

참조: https://wikidocs.net/122729

각 열마다 함수 태워서 새 행으로 추가하기

for user in user_df['user_seq']:
    user_df['notification_yn'] = weekend_night_off(user)

데이터프레임 리스트로 변환하기

device_token_list = user_df['device_token'].tolist()
# None 제거
device_token_list = [x for x in device_token_list if x is not None]
# 중복제거
device_token_list = list(dict.fromkeys(device_token_list))

cheat sheet

https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf

cheat sheet

나의 사용예시 0

sql문을 데이터프레임으로 읽어내는 코드이고 각각 데이터프레임, 리스트를 추출한다.

sql = 'select board_seq, user_seq from board'

board_df = pd.read_sql(sql, conn)
board_list = board_df.to_dict(orient='records') #1번
board_list = board_df['board_seq'].tolist() #2번

나의 사용예시 1

두 개의 데이터프레임을 합쳐서 목표주가가 달성되었는지 비교하고 달성된 게시글 번호에 해당하는 데이터를 조작하고 슬랙 메시지를 보내는 코드이다.

import pymysql
import pandas as pd
from config.config_db  import DataBaseConfig as con_DB
from configuration     import server_name, send_slack

def target_price_check(conn, conn_finance):
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    sql = f"""
            select a.board_seq, a.symbol, a.target_price
              from db_community.tb_board_best a inner join db_community.tb_board b
                                                        on a.board_seq = b.board_seq
                                                       and b.del_yn = 0
             where a.is_accuracy = 0 
               and a.target_price is not null and a.target_price != 0 
          """
    data_df = pd.read_sql(sql, conn)
    args_list = data_df.to_dict(orient='records')
    symbol_list = [ args['symbol'] for args in args_list ]
    # print('symbol_list', symbol_list)

    cursor = conn_finance.cursor(pymysql.cursors.DictCursor)
    finance_sql = f"""
            select symbol, adj_low_prc, adj_close_prc, adj_high_prc
              from dg_db_refine_daily.tb_price_tango_data 
             where trd_dt = date_format(DATE_ADD(now(), INTERVAL -1 DAY), '%Y%m%d')
               and symbol in ({str(symbol_list).strip('[]')})			
        """
    finance_df = pd.read_sql(finance_sql, conn_finance)
    # print('finance_df', finance_df)

    df = pd.merge(data_df, finance_df, how='left')
    # print('df', df)

    true_df = df.loc[(df['adj_low_prc'] < df['target_price']) & (df['target_price'] < df['adj_high_prc'])]
    # print('true_df', true_df)

    if true_df.empty:
        send_slack(f'목표주가 달성 게시글 없음', f' tangopick_{server_name}_accuracy')
    else:
        board_list = true_df['board_seq'].tolist()
        # print('board_list', board_list)

        cursor = conn.cursor(pymysql.cursors.DictCursor)
        update_sql = f"""
            update db_community.tb_board_best
               set is_accuracy = 1, accuracy_dt = now()
             where board_seq in ({str(board_list).strip('[]')})
        """
        cursor.execute(update_sql)
        conn.commit()
        # board_list = list(map(int, board_list))
        for board_seq in board_list:
            row = df.loc[df['board_seq'] == board_seq]
            send_slack(f' 목표달성글 : https://{""if server_name == "prod" else "dev."}tangopick.co.kr/community/1/board/{board_seq} \n'
                       f' {row} ',
                       f' tangopick_{server_name}_accuracy')

    return True

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

나의 사용예시 2

구독자에 따라 유저의 등급이 변하는 코드를 5분마다 한 번씩 돌려서 등급이 변한 유저가 있으면 해당 내용을 슬랙으로 보내는 코드이다.

def save_insert_query(df, table_nm, db_name):
    df = df.where(pd.notnull(df), None)
    rows = 0

    before_sql = f"""
            select user_seq, nickname, sub_cnt, grade
              from db_user.tb_user_grade
             where grade != 'influencer'
               and nickname != '탱고픽'
        """
    df_before = pd.read_sql(before_sql, conn_tangopick)

    columns = list(df.columns)
    header_sql = f"""insert into {db_name}.{table_nm}
                     ({str(columns).strip('[]').replace("'", '`')}) 
                     values 
                   """
    body_sql = str(['%s' for i in columns]).replace("[", "(").replace("]", ")").replace("'", "")
    key_placeholders = ', '.join(['`{0}`=VALUES(`{0}`)'.format(k) for k in columns])
    footer_sql = f" on duplicate key update {key_placeholders}"

    total_query = header_sql + body_sql + footer_sql

    cursor = conn_tangopick.cursor()
    affected_rows = cursor.executemany(total_query, df.values.tolist())
    conn_tangopick.commit()

    compare_result(df_before)

    return affected_rows


def compare_result(df_before):
    after_sql = f"""
            select user_seq, nickname, sub_cnt, grade
              from db_user.tb_user_grade
             where grade != 'influencer'
               and nickname != '탱고픽'
        """
    df_after = pd.read_sql(after_sql, conn_tangopick)

    print('df_before[grade]', df_before['grade'])
    print('df_before[grade] == df_after[grade]', df_before['grade'] == df_after['grade'])
    print('df_before[(df_before[grade] == df_after[grade]) == False', df_before[(df_before['grade'] == df_after['grade']) == False])

    before = df_before[(df_before['grade'] == df_after['grade']) == False]
    after = df_after[(df_before['grade'] == df_after['grade']) == False]
    num = len(df_after[(df_before['grade'] == df_after['grade']) == False])

    if ((datetime.now().hour == 10 and datetime.now().minute == 00) or (datetime.now().hour == 11 and datetime.now().minute == 00) or
        (datetime.now().hour == 13 and datetime.now().minute == 00) or (datetime.now().hour == 14 and datetime.now().minute == 00) or
        (datetime.now().hour == 15 and datetime.now().minute == 00) or (datetime.now().hour == 16 and datetime.now().minute == 17) or
        (datetime.now().hour == 17 and datetime.now().minute == 00) or (datetime.now().hour == 18 and datetime.now().minute == 00)):
        if after.empty:
            send_slack(f'유저등급조정없음', f' tangopick_{server_name}_grade')
        else:
            send_slack(f' 유저등급조정 : {num}명 조정, \n'
                       f' 조정전: {before[["user_seq", "nickname", "grade"]]}, \n'
                       f' 조정후: {after[["user_seq", "nickname", "grade"]]}',
                       f' tangopick_{server_name}_grade')
    return True

크론탭 (정해진 요일, 정해진 시간에 실행)

|

목적

크론탭은 정해진 스케줄에 파일이 실행되도록 해주는 기능이다.

crontab -e 명령어로 크론탭 실행명령어를 저장한다.

*/10 * * * * docker exec user python /home/ubuntu/app/batch/v3_1/batch_reserve_publish.py > /home/ubuntu/workspace/user/src/log/batch_reserve_publish_log.log 2>&1
#10분마다 계속 실행

0-59 9-23 * * 1-5 docker exec create_engine_v2 python /home/ubuntu/app/process_tool/v3_1/realtime/idea_update_period.py
#9시부터 11시까지 월-금 매분 실행

# target_accuracy
0 8 30 * * 2-6 docker exec user python /home/ubuntu/app/batch/v3_2/batch_price_accuracy.py > /home/ubuntu/app/batch/v3_2/batch_price_accuracy.log 2>&1
#화-토 매일 8시30분에 실행

https://crontab.guru/ 에 들어가면 크론탭 명령어를 가장 쉽게 뽑아낼 수 있다.

api 사용 화면

crontab -l 로 크론탭 파일상태를 확인할 수 있다.

슬랙봇을 붙여넣으면 파일이 돌아가고 있는 현황을 업데이트받을 수 있어서 편리하다.

예시:

import pymysql
from datetime             import datetime
from config.config_db     import DataBaseConfig as con_DB
from configuration        import server_name, send_slack

def delete_temp(conn, now):
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    sql = f"""
        update db_community.tb_board_draft
           set del_yn = 1, del_dt = '{now}'
         where reg_dt < DATE_ADD('{now}', INTERVAL -60 DAY)
    """
    affected_rows = cursor.execute(sql)

    assert affected_rows != -1, '쿼리 실행에 실패하였습니다'

    if affected_rows != 0:
        send_slack(f' 60일 지난 임시저장 게시글 {affected_rows}개가 삭제되었습니다 \n',
                   f' tangopick_{server_name}_temp')
    else:
        send_slack(f' 삭제한 임시저장 글이 없습니다 \n',
                   f' tangopick_{server_name}_temp')


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')

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

pk가 중복이 아닌 필드만 업데이트 하기(insert into values on duplicate key update 여러행 )

|

목적

insert into values르ㄹ 사용해서 pk가 중복이 아닌 경우에만 행을 업데이트하고 싶었다. 여러 행을 한 꺼번에 작업해야하는 경우에 아래와 같이 insert into 와 duplicate key update를 앞 뒤에 붙여서 한 번에 쓸 수 있었다.

mysql 코드

insert into db_community.tb_board_best (yymm, board_seq, SYMBOL, cmp_nm_kor, cmp_nm_eng, current_price, TARGET_PRICE, currency_cd, growth, PROFIT, SAFETY, DIVIDEND, OPINION,keypoint, category, logo, expect_return_val) VALUES 
('202206',16533, '078350.KQ','한양디지텍','hanyang digi','14200',26240,'KRW',5,3,3,2,4,'세상에는 어떤 변수가 일어날지 모른다. 모두가 된다 해도 망할 수 있으며 모두가 안된다 해도 흥할 수 있다. +능력 범위라는 것의 정확한 정의','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/DIS.png','14.0684410646388'),
('202206',16686, '010060.KS','OCI','oci','106000',110000,'KRW',3,3,2,1,2,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/078350.KQ.png','84.7887323943662'),
('202206',17444, '006400.KS','삼성SDI','samsung sdi co.,ltd.','508000',600000,'KRW',5,3,3,2,4,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/010060.KS.png','3.77358490566038'),
('202206',17468, '214270.KQ','FSN','fsn','7920',12000,'KRW',5,3,4,1,3,'광고 경기 회복에 따라 매출이 확대될 것으로 예상하며, 커머스 부문의 성장세와 신규 진출 블록체인 사업의 잠재력을 감안하면 현재의 주가는 저평가 되어있다고 생각함.','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/006400.KS.png','18.1102362204724'),
('202206',17694, '217820.KQ','엔에스','ns','10250',19500,'KRW',5,3,3,0,3,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/214270.KQ.png','51.
on duplicate key update board_seq = values(board_seq)

위의 코드는 pk가 중복될 경우에 행의 다른 필드는 업데이트하지 않는다.
다른 필드도 업데이트하려면 on duplicate key update 뒤에 컬럼을 명시해주어야한다.(참고:https://kkangdda.tistory.com/53)

replace into를 사용하고 싶었는데 그럼 행을 삭제하고 다시 추가하는 것이라고 해서 pk를 건드리지 않은 채로 놔두기 위해서 쓰지 않았다.

sql에서는 참고에 나와있는 것처럼 변수명으로 아름답게 해줄 수 없어서 아래와 같이 컬럼을 추가해주었다.
개발을 하면서 노가다가 이렇게 적성에 잘 맞는지 깨닫고 있다.

insert into db_community.tb_board_best (yymm, board_seq, SYMBOL, cmp_nm_kor, cmp_nm_eng, current_price, TARGET_PRICE, currency_cd, growth, PROFIT, SAFETY, DIVIDEND, OPINION,keypoint, category, logo, expect_return_val) VALUES 
('202206',16533, '078350.KQ','한양디지텍','hanyang digi','14200',26240,'KRW',5,3,3,2,4,'세상에는 어떤 변수가 일어날지 모른다. 모두가 된다 해도 망할 수 있으며 모두가 안된다 해도 흥할 수 있다. +능력 범위라는 것의 정확한 정의','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/DIS.png','14.0684410646388'),
('202206',16686, '010060.KS','OCI','oci','106000',110000,'KRW',3,3,2,1,2,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/078350.KQ.png','84.7887323943662'),
('202206',17444, '006400.KS','삼성SDI','samsung sdi co.,ltd.','508000',600000,'KRW',5,3,3,2,4,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/010060.KS.png','3.77358490566038'),
('202206',17468, '214270.KQ','FSN','fsn','7920',12000,'KRW',5,3,4,1,3,'광고 경기 회복에 따라 매출이 확대될 것으로 예상하며, 커머스 부문의 성장세와 신규 진출 블록체인 사업의 잠재력을 감안하면 현재의 주가는 저평가 되어있다고 생각함.','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/006400.KS.png','18.1102362204724'),
('202206',17694, '217820.KQ','엔에스','ns','10250',19500,'KRW',5,3,3,0,3,'','B010801','https://cloudfront.alpha-bridge.kr/prod/image/logos/214270.KQ.png','51.5151515151515')
on duplicate key update yymm =values(yymm), symbol = values(symbol), cmp_nm_kor = values(cmp_nm_kor), cmp_nm_eng=values(cmp_nm_eng), current_price=values(current_price),
TARGET_PRICE=values(TARGET_PRICE),currency_cd=values(currency_cd), growth = values(growth), PROFIT = values(PROFIT), SAFETY=values(SAFETY), DIVIDEND=values(DIVIDEND), OPINION=values(OPINION), keypoint=values(keypoint),
category= values(category), logo=values(logo), expect_return_val=values(expect_return_val)