Python 으로 if 를 활용한 분기문이나 for, while 을 활용한 반복문 프로그래밍에 들어가기에 앞서서, if, for, while 프로그래밍의 기초 지식이 되는 블리언 형식에 대해서 알아보겠습니다. 


이번 포스팅은 어려운건 없구요, 그냥 편안하게 몸풀기 (머리 풀기?) 정도로 읽어보면 좋겠습니다. 

(not 논리 연산자가 살짝 헷갈리기는 합니다만, 전반적으로 매우 쉬움)


블리언 형식(Boolean type)참(True), 거짓(False) 의 두 개 값을 가지는 자료형을 말합니다. 



# (1) Boolean Type

In [1]: a = 2 > 1


In [2]: a

Out[2]: True


In [3]: b = 2 < 1


In [4]: b

Out[4]: False


In [5]: type(a)

Out[5]: bool

 


불리언 값이 (1) 조건문, (2) 논리 연산자, (3) 비교 연산자에서 어떤 경우에 참(True)이고, 어떤 경우에 거짓(False) 인지에 대해서 짚고 넘어가 보겠습니다. 



[ 파이썬 참/거짓 불리언 형식 (Python Boolean Type ]





 (1) 조건문에서의 참, 거짓 (True, False in Conditional statements)


프로그래밍의 조건문에서 참, 거짓을 판단할 때 블리언 False, None, 숫자 0, 비어있는 리스트, 비어 있는 튜플, 비어있는 사전 자료형의 경우에 False 로 판단을 합니다. 


반대로 블리언 True, not None, 0이 아닌 숫자, 값이 있는 리스트, 값이 있는 튜플, 값이 있는 사전 자료형의 경우 True 로 판단을 하구요. 


자료형마다 값이 없이 비어있으면 거짓, 값을 가지고 있으면 참이라고 파이썬은 판단한다는게 재미있습니다. ^^


각 경우 마다 bool() 함수를 사용해서 파이썬이 참(True)과 거짓(False) 중에 어떻게 판단을 하는지 예를 들어서 살펴보겠습니다. 



# (1-1) Boolean False, True

In [6]: bool(False) # False

Out[6]: False


In [7]: bool(True) # True

Out[7]: True

 



# (1-2) None is False

In [8]: bool(None) # False

Out[8]: False

 



# (1-3) Number 0, 0.00 is False

In [9]: bool(0) # False

Out[9]: False


In [10]: bool(0.00) # False

Out[10]: False


In [11]: bool(5) # True

Out[11]: True

 



# (1-4) blank List [] is False

In [12]: bool([]) # False

Out[12]: False


In [13]: bool(['a', 'b']) # True

Out[13]: True

 



# (1-5) blank Tuple () is False

In [14]: bool(()) # False

Out[14]: False

In [15]: bool(('a', 'b')) # True

Out[15]: True

 



# (1-6) blank Dictionary {} is False

In [16]: bool({}) # False

Out[16]: False

In [17]: bool({'a': 'b'}) # True

Out[17]: True

 




  (2) 논리 연산자에서 참, 거짓 (True, False in Logical operators) : not, and, or


파이썬의 논리 연산자(Logical operators)에는 피연산자를 부정하는 not, 두 피연산자 간의 논리곱을 수행하는 and, 두 연산자 간의 논리합을 수행하는 or 의 3가지가 있습니다. 



(2-1) 피연산자를 부정하는 'not' 논리 연산자 


위에서 소개한 조건문에서의 참, 거짓에 not 연산자를 붙이면 참이 거짓으로 바뀌고, 거짓은 참으로 바뀌게 됩니다. 



# (2-1) 피연산자 부정 'not' logical operator


In [18]: not True # False

Out[18]: False


In [19]: not False # True

Out[19]: True

 



숫자 '0'은 거짓(False)라고 했으므로 앞에 부정 연산자 not 이 붙으면 참(True)이 됩니다. 반면, '0'이 아닌 숫자는 참(True)이라고 했으므로 앞에 not 이 붙으면 거짓(False)으로 바뀝니다. 



In [20]: not 0 # True

Out[20]: True


In [21]: not 5 # False

Out[21]: False

 



None 은 거짓(False)라고 했으므로 not None은 참(True)로 평가합니다. 



In [22]: not None # True

Out[22]: True

 



비어있는 문자열, 리스트, 튜플, 사전은 거짓(False)으로 평가한다고 했으므로, 피연산자를 부정하는 not 이 붙으면 참(True)으로 바뀌게 됩니다. 



In [23]: not '' # denial of blank String -> True

Out[23]: True


In [24]: not [] # denial of blank List -> True

Out[24]: True


In [25]: not () # denial of blank Tuple -> True

Out[25]: True


In [26]: not {} # denial of blank Dictionary -> True

Out[26]: True

 



반대로, 문자열, 리스트, 튜플, 사전에 값이 들어있는 경우 참(True)으로 평가한다고 했으므로, 피연산자를 부정하는 not이 붙으면 거짓(False)이 됩니다.  위와 아래의 비어있는 자료형에 not 붙인 경우와 값이 있는 자료형에 not 붙인 경우의 참, 거짓이 처음엔 좀 직관적으로 와닿지가 안던데요, 자꾸 보니깐 그러려니 하게 되네요. ^^'



In [27]: not 'ab' # False

Out[27]: False


In [28]: not ['a', 'b'] # False

Out[28]: False


In [29]: not ('a', 'b') # False

Out[29]: False


In [30]: not {'a' : 'b'} # False

Out[30]: False

 




(2-2) 두 피연산자 간의 논리곱을 수행하는 and 논리 연산자


두 개의 피연산자가 모두 참(True and True)이면 True 이며, 두 피연산자 값 중에서 하나라도 거짓(False)이 있으면 거짓(False) 으로 판단합니다. 



# (2-2) 논리곱 'and' logical operator

In [31]: True and True # True

Out[31]: True


In [32]: True and False # False

Out[32]: False


In [33]: False and True # False

Out[33]: False

 

In [34]: False and False # False

Out[34]: False





(2-3) 두 피연산자 간의 논리합을 수행하는 or 논리 연산자


두 피연산자 중에서 한 개(True or False, False or True)나 혹은 두개 모두(True or True) 이라도 참(True)이면 참으로 평가합니다. 



# (2-3) 논리합 'or' logical operator

In [35]: True or True # True

Out[35]: True


In [36]: True or False # True

Out[36]: True


In [37]: False or False # False

Out[37]: False

 




  (3) 비교 연산자에서의 참, 거짓 (True, False in Comparison operators) : ==, !=, >, >=, <, <=


(3-1) == 비교 연산자


두 피연산자의 값이 같으면 참(True), 서로 다르면 거짓(False)으로 평가



# (3) Comparison(Relational) operators


In [39]: a = 1; b = 2; c = 2 # input data



# (3-1) == : If the values of two operands are equal, then the condition becomes true


In [40]: a == b # False

Out[40]: False


In [41]: b == c # True

Out[41]: True

 



(3-2) != 비교 연산자


두 피연산자의 값이 같지 않으면 참(True), 같으면 거짓(False)으로 평가 (음... 좀 헷갈리지요. ^^;)



In [39]: a = 1; b = 2; c = 2 # input data

# (3-2) != : If values of two operands are not equal, then condition becomes true

In [42]: a != b # True

Out[42]: True


In [43]: b != c # False

Out[43]: False

 



(3-3) > 비교 연산자


: 왼쪽의 피연산자 값이 오른쪽의 피연산자 값보다 크면 참(True), 아니면 거짓(False)



In [39]: a = 1; b = 2; c = 2 # input data


# (3-3) > : If the value of left operand is greater than the value of right operand, 

# then condition becomes true


In [44]: a > b # False

Out[44]: False


In [45]: b > a # True

Out[45]: True


In [46]: b > c # False

Out[46]: False

 



(3-4) >= 비교 연산자


왼쪽의 피연산자 값이 오른쪽의 피연산자 값보다 크거나 같으면 참(True), 아니면 거짓(False)



In [39]: a = 1; b = 2; c = 2 # input data


# (3-4) >= : If the value of left operand is greater than or equal to the value of right operand, 

# then condition becomes true


In [47]: a >= b # False

Out[47]: False


In [48]: b >= a # True

Out[48]: True


In [49]: b >= c # True

Out[49]: True

 



(3-5) < 비교 연산자


왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 작으면 참(True), 아니면 거짓(False)



In [39]: a = 1; b = 2; c = 2 # input data

# (3-5) < : If the value of left operand is less than the value of right operand, 

# then condition becomes true


In [50]: a < b # True

Out[50]: True


In [51]: b < a # False

Out[51]: False


In [52]: b < c # False

Out[52]: False

 



(3-6) <= 비교 연산자


왼쪽 피연산자의 값이 오른쪽 피연산자의 값보다 작거나 같으면 참(True), 아니면 거짓(False)



In [39]: a = 1; b = 2; c = 2 # input data


# (3-6) <= : If the value of left operand is less than or equal to the value of right operand,
# then condition becomes true


In [53]: a <= b # True

Out[53]: True


In [54]: b <= a # False

Out[54]: False


In [55]: b <= c # True

Out[55]: True

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾸욱 눌러주세요. ^^


다음번 포스팅에서는 if ~ else 분기문(Branch statement)에 대해서 알아보겠습니다. 



저작자 표시 비영리 변경 금지
신고
Posted by R Friend R_Friend

보통 외부 데이터 불러오기 할 때 {utils} package의 read.table() 함수를 사용하곤 합니다. 


크기가 작은 데이터라면 별 문제를 못느낄 텐데요, 만약 데이터 사이즈가 수 메가, 기가 단위의 큰 데이터라면 데이터를 불러들이는데 너무 오랜 시간이 걸려서 문제가 될 수 있습니다. 


대용량 데이터 처리에 아주 뛰어난 성능을 발휘하는 data.table 패키지의 fread() 함수를 사용하면 큰 용량의 외부 데이터도 빠르게 불러올 수 있습니다. 


(물론 R은 메모리에 데이터를 올려놓고 처리/분석을 하므로 하둡에서 말하는 수테라급의 대용량에는 필적을 못하구요, 분산병렬처리도 아니긴 합니다. 이 포스팅에서 말하는 대용량은 책보고 공부할 때 사용하는 수십, 수백개 row 를 가진 예제 데이터 대비 실전에서 사용하는 수십만, 수백만, 수천만 row 데이터를 말하는 것입니다. ^^;)


아래에 간단한 샘플 데이터를 만들어서 {utils} 패키지의 read.table() 함수와 {data.table} 패키지의 fread() 함수의 데이터 불러오는데 소요되는 시간을 비교해보았습니다. 



[ 외부 데이터 읽어오기 : {utils} 패키지 read.table() 함수 vs. {data.table} 패키지 fread(0 함수 ]




1. 샘플 데이터 만들기 : 1 백 만개 row, 변수 2개를 가지는 데이터 프레임



# generating large scaled data

my_data <- data.frame(var1 = rnorm(n = 1000000, mean = 0, sd = 1), 

                      var2 = rnorm(n = 1000000, mean = 2, sd = 3))

 



> str(my_data)

'data.frame': 1000000 obs. of  2 variables:

 $ var1: num  -0.556 1.787 0.498 -1.967 0.701 ...

 $ var2: num  1.669 0.597 4.452 1.405 6.936 ...

 



# exporting to text file

write.table(my_data, 

            "/Users/Desktop/R/my_data.txt",

            sep = "|",

            row.names = FALSE, 

            quote = FALSE)

 




2. {utils} 패키지의 read.table() 함수를 사용해서 my_data.txt 불러오기


system.time() 함수로 데이터를 불어오는데 소요된 시간을 재어보았더니 7.287초가 나왔습니다. 

(매번 할 때마다 소요 시간이 조금씩 차이가 날 수는 있습니다)



# reading text file : (1) read.table() of {utils} package

> system.time(my_data_readtable <- read.table("/Users/Desktop/R/my_data.txt",

+                                             sep = "|", 

+                                             header = TRUE, 

+                                             stringsAsFactors = FALSE))

   user  system elapsed 

  7.161   0.100   7.287

 




3. {data.table} 패키지의 fread() 함수를 사용해서 my_data.txt 불러오기


data.table 패키지는 기본 패키지가 아니므로 먼저 별도 설치(install.packages) 및 호출(library)이 필요합니다. 

(매번 할 때마다 소요 시간이 조금씩 차이가 날 수는 있습니다)



# reading text file : (2) fread() of {data.table} package

install.packages("data.table")

library(data.table)

 



system.time() 함수로 my_data를 불러오는데 걸린 시간을 재어봤더니 0.256초가 걸렸습니다. 



> system.time(my_data_fread <- fread("/Users/Desktop/R/my_data.txt", 

+                                    sep = "|", 

+                                    header = TRUE, 

+                                    stringsAsFactors = FALSE))

   user  system elapsed 

  0.242   0.014   0.256

 




1백만 행을 가진 데이터프레임을 읽어오는데 있어, 앞서 read.table() 함수가 7.287 초 걸렸던데 반해, fread() 함수는 0.256 초밖에 걸리지 않았습니다.  fread() 함수는 read.table() 함수를 사용했을 때 대비 약 96.5% 정도 속도가 더 적게 걸린 것입니다.  놀랍지요?!!! 



> 0.256/7.287

[1] 0.03513106

 



R 은 대용량 데이터에는 맥을 못춰라고 지레 평가절하하기 보다는 {data.table} 패키지의 fread() 함수로 대용량 데이터 불러오기 속도 문제를 공략해보시지요. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'을 꾸욱 눌러주세요. ^^


저작자 표시 비영리 변경 금지
신고
Posted by R Friend R_Friend

대용량 데이터를 관계형 DB나 HDFS 파일로 저장해놓고 필요한 대상만 데이터 전처리, 샘플링해서 R에서 통계분석, 기계학습을 하는 경우가 많이 있습니다. 


이전 포스팅에서 R로 Hive와 PostgreSQL, MySQL 등에 DB connect 해서 사용하는 방법을 소개했었습니다. 


이번 포스팅에서는 


 - R로 Oracle DB connect 하고 데이터 query 하는 방법

 - R로 Presto DB connect 하고 데이터 query 하는 방법


을 소개하겠습니다. 





 1. R로 Oracle DB connect 하고 데이터 query 하는 방법


- rJava, DBI, RJDBC 패키지는 의존성이 있으므로 순서대로 설치하시기 바랍니다. (순서가 바뀌면 에러 발생)

- 혹시 Java 가 설치 안되어 있거나, 버전이 안맞아서 새로 설치해야 하면 이곳 참조 => http://rfriend.tistory.com/232

- Oracle DB의 SQL query문 마지막에 세미콜론(';')을 사용하면 에러가 나므로 유의하세요. 



# R Oracle DB Connect

 

install.packages('rJava')
install.packages('DBI')
install.packages('RJDBC')

 

library(rJava)
library(DBI)
library(RJDBC)

 


# setting driver and connection configuration


drv_Oracle <- JDBC(driverClass="oracle.jdbc.driver.OracleDriver", classPath="C:/download/ojdbc7.jar")

 

con_Oracle <- dbConnect(drv_Oracle, 
                                         "jdbc:oracle:thin:@//12.34.567.89:8888/DB_name",  # ip:port
                                         "id", 
                                         "password")

 

# SQL query
query <- "
       SELECT *
            FROM db.table
            WHERE var1 = 'aaa'
                  AND var2 = 'bbb' " # Do not use ';' (semicolon)

 

# executing DB connect & query
my_data <- dbGetQuery(con_Oracle, query)

 

# DB disconnect
dbDisconnect(con_Oracle)

 

# delete temp objects
rm(drv, con_Oracle, query)

 




 2. R로 Presto DB connect 하고 데이터 query 하는 방법


Presto는 Facebook에서 개발해서 오픈소스로 푼 분산 SQL 쿼리 엔진(distributed SQL query engine for big data)입니다. 메모리 기반으로 실행이 되다 보니 Hive 보다 속도가 많이 빠르기도 하구요, 또 Hive, phoenix, mysql 등 이기종 DB에 접속해서 데이터를 취합하고 가공하는 작업을 할 수 있어서 매우 편리하기도 합니다. 



# R Presto DB Connect


# install and request package

install.packages("RPresto")

library(RPresto)


# setting dbConnect

con_RPresto <- dbConnect(

                                           RPresto::Presto(), 

                                           host='http://12.34.56.789', 

                                           port=8080,

                                           user='user_id',

                                           schema='schema_nm',

                                           catalog='hive', # mysql, phoenix..

                                           session.timezone='UTC' 

                                           )


query <- paste0("

    SELECT var1, var2, count(*) as cnt

        FROM db.table

        WHERE var1 = 'aa'

            AND var2 = 'bb'

        GROUP BY var1, var2

")


# executing query

my_data <- dbGetQuery(con_RPresto, query)


# disconnecting DB

dbDisconnect(conn = con_Postgres)



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾸욱 눌러주세요. ^^



저작자 표시 비영리 변경 금지
신고
Posted by R Friend R_Friend

파이썬(Python)에는 다양한 형태의 다수의 데이터를 다룰 수 있는 자료형으로 리스트(Lists), 튜플(Tuples), 사전(Dictionary) 등이 있습니다. 


지난번 포스팅에서는 사전(Python Dictionary) 자료형의 생성, 삭제, 기본 연산자에 대해서 소개하였습니다.  이번 포스팅에서는 지난번에 이어서, 사전 자료형의 내장 함수 및 메소드(Dictionary built-in functions and methods)에 대해서 알아보겠습니다. 


사전 자료형은 키와 값의 쌍(pair of Key and Value)으로 이루어져 있는 형태에 맞게 이에 특화된 메소드들이 있습니다. 



[ 파이썬 사전 자료형의 내장 함수 및 메소드 (Python Dictionary built-in functions and methods) ]






1. 파이썬 사전 자료형의 내장 함수 (Python Dictionary built-in functions)


 1-1. len(dict): 사전의 총 길이 (total length of Dictionary)



# 1-1. len(dict) : Gives the total length of the dictionary

>>> dict_1 = {'name' : 'Python Friend', 

...           'region' : 'Busan, Korea', 

...           'phone' : '010-123-9876', 

...           'age' : 30,

...           'name' : 'R Friend'}

>>> dict_1

{'name': 'R Friend', 'region': 'Busan, Korea', 'phone': '010-123-9876', 'age': 30}

>>> len(dict_1)

4

 




 1-2. str(dict): 사전을 문자열로 반환 (string representation of a Dictionary)



# 1-2. str(dict) : Produces a printable string representation of a dictionary

>>> str(dict_1)

"{'name': 'R Friend', 'region': 'Busan, Korea', 'phone': '010-123-9876', 'age': 30}"





 1-3. type(variable): 입력 변수의 유형 반환 (returns the type of the passed variable)



# 1-3. type() : Returns the type of the passed variable

>>> type(dict_1)

<class 'dict'>

 





2. 파이썬 사전 자료형의 메소드 (Python Dictionary methods)


 2-1. dict.keys(): 사전의 키 목록



# 2-1. dict.keys(): Returns a list of all the available keys in the dictionary

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_2.keys()

dict_keys(['key1', 'key2', 'key3'])

 




 2-2. dict.values(): 사전의 값 목록



# 2-2. dict.values(): Returns a list of all the values available in the dictionary

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_2.values()

dict_values([123, 'abc', (1, 2, 3)])

 




 2-3. dict.items(): 사전의 (키, 값) 튜플 목록 



# 2-3. dict.items(): Returns a list of dict's (key, value) tuple pairs

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_2.items()

dict_items([('key1', 123), ('key2', 'abc'), ('key3', (1, 2, 3))])

 




 2-4. dict.clear(): 사전의 모든 {키, 값} 셋 제거



# 2-4. dict.clear() : Removes all items from the dictionary

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_2.clear()

>>> dict_2

{}

 




 2-5. dict.copy(): 사전의 {키 : 값} 셋 복사



# 2-5. dict.copy(): Returns a copy of the dictionary

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

dict_2

>>> {'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_3 = dict_2.copy()

>>> dict_3

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

 




 2-6. dict.fromkeys(seq, value): seq, value 셋으로 신규 사전 생성



# 2-6. dict.fromkeys(): Creates a new dictionary with keys from seq and values set to value

>>> seq = ('key1', 'key2', 'key3')

>>> 

>>> dict_4 = dict.fromkeys(seq)

>>> dict_4

{'key1': None, 'key2': None, 'key3': None}

>>> 

>>> dict_4 = dict.fromkeys(seq, 123)

>>> dict_4

{'key1': 123, 'key2': 123, 'key3': 123}

 




2-7. dict.get(key, default=None): 키에 할당된 값 반환



# 2-7. dict.get(key, default=None): Returns a value for the given key

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

          'key3' : (1, 2, 3)}

... >>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

>>> dict_2.get('key1', 'No_Key')

123

>>> dict_2.get('key5', 'No_Key') # If key is not available then returns 'No_Key'

'No_Key'

>>> dict_2.get('key5') # If key is not available then returns default value None

 




2-8. dict.setdefault(key, default=None) : 키에 할당된 값 반환


dict.get()과 유사합니다. 



# 2-8. dict.setdefault(key, default=None): Returns the key value available in the dictionary

>>> dict_2 = {'key1' : 123, 

...           'key2' : 'abc', 

...           'key3' : (1, 2, 3)}

>>> 

>>> dict_2

{'key1': 123, 'key2': 'abc', 'key3': (1, 2, 3)}

>>> 

dict_2.setdefault('key1', 'No-Key')

>>> 123

>>> dict_2.setdefault('key5', 'No_Key') 

'No_Key'

>>> dict_2.setdefault('key6', None)

 




2.9. dict.update(dict2): 기존 사전에 새로운 사전 dict2 추가



# 2-9. dict.update(dict2): Adds dictionary dict2's key-values pairs into dict

>>> dict_5 = {'key1' : 12, 

...           'key2' : 34}

>>> dict_5

{'key1': 12, 'key2': 34}

>>> 

>>> dict_6 = {'key3' : 56}

>>> 

>>> dict_5.update(dict_6)

>>> dict_5

{'key1': 12, 'key2': 34, 'key3': 56}

 



드디어 파이썬의 5가지 자료형인 숫자(Number), 문자열(String), 리스트(List), 튜플(Tuple), 그리고 사전(Dictionary)에 대한 생성, 기본 사용법, 내장함수 및 메소드에 대한 소개를 마쳤습니다. 


가장 기본이 되는 것이고 매우 매우 중요한 것이다 보니 블로그에 한번 정리를 해야지, 해야지... 하다가도 numpy 랑 pandas 먼저 포스팅하고.... 한참이 지나서야 기본이 되는 자료구조에 대해서는 이제서야 포스팅을 하게 되었네요.  밀린 숙제 끝낸 기분이라 홀가분하고 좋네요. ^^


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾸욱 눌러주세요. ^^


저작자 표시 비영리 변경 금지
신고
Posted by R Friend R_Friend