※ Elliott Pattern Helper Add In
지난 글에서 차트의 스크롤 기능을 구현해 보았습니다. 그런데 DDE를 통해 체결정보를 수신하여 캔들정보 시트에 반영할 때, 캔들의 저가나 고가 또는 종가가 변경될 뿐만 아니라 캔들의 시간단위마다 새로운 캔들이 생성될텐데요, 이를 차트에도 반영해야 할 것입니다.
이번 글의 주제는 이미 생성되어 있는 차트를 DDE 이벤트에 따라 refresh하는 것입니다. ※ (참고) DDE 이벤트는 현재가, 체결량 등 DDE Item의 값이 변하는 것을 말합니다.
차트의 스크롤 기능을 구현했던 과정을 상기해 보면, 차트의 데이터 소스의 값들은 실제 데이터가 아니라 실제 데이터를 가르키는 셀 수식(OFFSET)이었습니다. 따라서 캔들의 저가와 고가 및 종가는 별다른 처리를 하지 않아도 자체로 update됩니다.
결국 새로운 캔들이 생성될 때만 차트를 refresh하면 되겠지요. 이를 위해 기존에 작성했던 'CCandleFeeder' 클래스에 'refreshChart' 함수를 하나 추가하고 필요한 부분을 수정할 것입니다.
계속 진행하기에 앞서 출력되는 차트의 크기(width, height, length)를 소스에 하드코딩했던 것에서 사용자의 입력을 받아 처리할 수 있도록 수정하겠습니다.
- (수정 전 시나리오) 시트 위에서 종목명을 선택하고 'Chart' 버튼을 클릭하면 차트 출력 시트가 생성되면서 해당 종목의 캔들 차트가 표시된다.
- (수정 후 시나리오) 시트 위에서 종목명을 선택하고 'Chart' 버튼을 클릭하면 차트 크기를 지정하는 폼이 팝업된다. 크기를 지정하고 'OK' 버튼을 클릭하면 차트 출력 시트가 생성되면서 해당 종목의 캔들 차트가 표시된다.
다음 주제(기술적 지표)를 감안하여 다중 페이지가 포함된 UserForm(사용자 정의 폼)을 생성하여 사용합니다.
엑셀에서 메뉴 '개발 도구' → 'Visual Basic'을 선택하거나 키보드의 'Alt + F11'을 눌러 VBA 편집창을 띄웁니다.
VBAProject 트리 목록에서 현재 작업 중인 파일의 Context 메뉴 → '삽입' → '사용자 정의 폼'를 선택하여 폼을 생성한 후 다음 그림 1과 같이 폼의 이름을 'FChart'로 수정하고, Caption은 적절히 입력해 줍니다.
그림 1-1. UserForm 'FChart' 생성 1 |
도구상자의 '다중 페이지'를 선택(그림 1-2의 1)하고, Form 위에 적당히 위치시킵니다(그림 1-2의 2).
그림 1-2. UserForm 'FChart' 생성 2 |
도구상자의 '텍스트 상자'를 선택(그림 1-3의 1)하고, Form 위에 적당히 위치시킨 뒤 레이블을 'Width'로 만들어 줍니다(그림 1-3의 2). 텍스트 상자의 이름은 'TextBoxCW'로 수정합니다(그림 1-3의 3).
그림 1-3. UserForm 'FChart' 생성 3 |
그림 1-3과 동일한 방법으로 차트의 높이와 캔들 수를 입력받기 위한 텍스트 상자를 'TextBoxCH'와 'TextBoxCL'의 이름으로 생성하고(그림 1-4의 1), 페이지의 이름을 적절히 수정합니다(그림 1-4의 2). ※ 텍스트 상자의 이름을 정확히 입력하셨는지 다시 한 번 확인하세요.
그림 1-4. UserForm 'FChart' 생성 4 |
다음 그림과 같이 도구상자의 '명령 단추'를 선택(그림 1-5의 1)하여 버튼을 생성하고(그림 1-5의 2) 이름을 'ButtonOK'로 수정합니다(그림 1-5의 3). Caption은 적절히 입력하십시요. 동일한 방법으로 'ButtonCancel'의 이름으로 버튼을 생성합니다.
그림 1-5. UserForm 'FChart' 생성 5 |
'ButtonCancel'을 더블클릭하여 '코드 보기' 모드로 들어가 아래와 같이 입력합니다.
그림 2. 'ButtonCanel' 이벤트 입력 |
※ ('ButtonCancel_Click' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Sub ButtonCancel_Click()
Unload Me
End Sub
Unload Me
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
폼 'FChart'를 초기화하기 위한 Sub모듈을 그림 3과 같이 입력합니다.
그림 3. 폼 초기화 모듈 입력 |
※ ('UserForm_Initialize' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Sub UserForm_Initialize()TextBoxCW.Value = 500
TextBoxCH.Value = 350
TextBoxCL.Value = 100
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
VBAProject 트리 목록에서 폼 'FChart'의 Context 메뉴 → '개체 보기'를 선택하여 개체 편집 모드로 들어간 후 'ButtonOK'를 더블클릭하면 버튼의 이벤트 처리 모듈 'ButtonOK_Click'의 뼈대가 자동으로 생깁니다. 다음 그림과 같이 편집하겠습니다.
그림 4. 'ButtonOK' 이벤트 입력 |
※ ('ButtonOK_Click' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Sub ButtonOK_Click()Call doCharting(Selection.Cells(1, 1).Value, _
CInt(TextBoxCW.Value), _
CInt(TextBoxCH.Value), _
CInt(TextBoxCL.Value))
Me.Hide
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 지난 글에서 작성했던 'doCharting'을 호출하는 것으로 하되, 인자를 세 개 더 넘겨줍니다. ※ 'doCharting'은 아래에서 수정하도록 하겠습니다.
폼 작성은 완료되었으니 시트의 'Chart' 버튼을 클릭했을 때 폼이 팝업되도록 수정해야 합니다. 버튼 'Chart'의 이벤트 처리 매크로 'Chart_Click'을 다음과 같이 수정합니다.
※ (수정 후 'Chart_Click' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub Chart_Click()Dim frm As FChart
Set frm = New FChart
Load frm
frm.Show
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
폼의 'OK' 버튼을 클릭하면 차트 크기(width × height × length)를 인자로 넘겨주면서 'doCharting'을 호출하도록 기존의 'doCharting'을 수정하겠습니다. 다음 소스로 대체하시면 됩니다.
※ (수정 후 'doCharting' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub doCharting(sn, cw, ch, cl)sn_cs = sheets_cs(sn)
sn_Arr = Split(sn_cs, ",")
If UBound(sn_Arr) < 0 Then Exit Sub
If cw = Empty Or cw = xlNullString Or cw < 50 Then cw = CHART_W
If ch = Empty Or ch = xlNullString Or ch < 50 Then ch = CHART_H
If cl = Empty Or cl = xlNullString Or cw < 0 Then cl = CHART_L
If createChartSheet(CHART_POSTFIX, sn_cs, xlStockOHLC, cw, ch, cl) Then
Sheets(CHART_POSTFIX).Select
Call onChartChange(xlStockOHLC, cw, ch, cl, sn_Arr(LBound(sn_Arr)))
End If
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 위 소스를 보시면 변수 'CHART_W', 'CHART_H', 'CHART_L'과 'CHART_POSTFIX'가 선언도 되지 않은채 사용되고 있음을 알 수 있습니다. 이들은 차트 크기의 기본값과 차트출력 시트의 이름을 저장하는 상수입니다. 그림 5와 하단 소스를 참고하여 선언하십시요.
그림 5. 기존 모듈 수정 |
※ (상수 선언)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Const CHART_POSTFIX = "CHARTS" 'Chart sheet namePublic Const CHART_W = 500 'Default chart width
Public Const CHART_H = 350 'Default chart height
Public Const CHART_L = 100 'Default chart length
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
현재까지 작업한 내용을 저장하고 테스트를 해 보겠습니다. 기존에 생성되어 있는 'CHARTS' 시트는 삭제한 후 'Chart' 버튼을 클릭하십시요.
그림 6. 폼 테스트 |
차트 크기를 조절한 후 'OK' 버튼을 클릭하면 새롭게 차트출력 시트가 생성되면서 지정한 크기로 차트가 출력됩니다.
그림 7. 차트 테스트 |
이제 원래 진행하고자 했던 주제 'DDE 이벤트를 반영한 차트 Refresh' 기능을 추가하겠습니다.
'CCandleFeeder' 클래스는 DDE 이벤트에 따라 여러 종목의 캔들정보 시트를 update하기 위해 작성했습니다. 클래스 소스를 보면, 'CCandleFeeder'가 feed를 하는 시트는 멤버 변수 'pCandleSheet'에 저장된 값임을 어렵지 않게 파악할 수 있을 것입니다. 그렇다면 현재 차트출력 시트에 있는 차트가 동일한 시트에 대한 차트인지 어떻게 판단할 수 있을까요?
간단한 방법이 하나 있습니다. 엑셀의 ChartObject에는 이름을 붙일 수 있으므로, 차트를 생성할 때 이름을 붙여주고 차트가 전환될 때에는 이름을 바꿔주는 것이죠. 우선, 그 작업을 하겠습니다. 먼저 'drawChartTWH'를 수정하겠습니다.
※ (수정 후 'drawChartTWH' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function drawChartTWH(ParamArray varArgs() As Variant)Dim cht As ChartObject
Dim CHTsheet As Worksheet
Set CHTsheet = ActiveSheet
If UBound(varArgs) > 3 Then
If varArgs(4) = xlNullString Then GoTo UseActiveSheet:
If Not sheetExist(varArgs(4)) Then
Set CHTsheet = Worksheets.Add(After:=Worksheets(Worksheets.Count))
CHTsheet.Name = varArgs(4)
Else
Set CHTsheet = sheets(varArgs(4))
End If
UseActiveSheet:
End If
po_y = CHTsheet.ChartObjects.Count * (2 * Rows("1:1").RowHeight + varArgs(3)) _
+ 2 * Rows("1:1").RowHeight
Set cht = CHTsheet.ChartObjects.Add(Left:=0, Width:=varArgs(2), _
Top:=po_y + Rows("1:1").RowHeight, _
Height:=varArgs(3))
cht.Chart.SetSourceData Source:=varArgs(0)
cht.Chart.ChartType = varArgs(1)
If UBound(varArgs) > 4 Then
If varArgs(5) <> xlNullString Then
With CHTsheet.ScrollBars.Add(0, po_y, varArgs(2), Rows("1:1").RowHeight)
'.Name = "ChartScroll"
.Min = 0
.Max = varArgs(6)
.Value = varArgs(6)
.SmallChange = 1
.LargeChange = 1
.LinkedCell = varArgs(5)
.Display3DShading = True
End With
End If
End If
If UBound(varArgs) > 6 Then
cht.Name = varArgs(7)
End If
Set CHTsheet = Nothing
Set cht = Nothing
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 하이라이트 된 부분만 추가하면 됩니다. 여덟 번째 인자 varArgs(7)이 있을 경우 차트에 이름을 붙이라는 것입니다. 인자를 ParamArray로 넘겨주는 이점이 있군요.
차트가 전환될 때 호출되는 'onChartChange'도 약간 수정해야 합니다. 다음 소스를 참고하여 수정하십시요.
※ (수정 후 'onChartChange' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub onChartChange(ParamArray varArgs() As Variant)If UBound(varArgs) < 3 Then _
Err.Raise 8902, "onChartChange", "ParamArray too short"
Dim chtRng As String, sn As String
On Error GoTo EH_onChartChange:
sn = Cells(2, 2).Value
If UBound(varArgs) > 3 Then sn = varArgs(4)
If sn = xlNullString Then Exit Sub
csheet = ActiveSheet.Name
chtRng = fillChartRng(csheet & "_AUX", sn, False, 2, varArgs(3))
sheets(csheet).Select
endRow = sheets(sn).Cells(1, 1).End(xlDown).Row
If ActiveSheet.ChartObjects.Count = 0 Then
drawChartTWH sheets(csheet & "_AUX").Range(chtRng), _
varArgs(0), varArgs(1), varArgs(2), csheet, _
csheet & "_AUX" & "!" & Cells(1, 1).Address, _
WorksheetFunction.Max(endRow - varArgs(3) - 1, 0), _
sn
Else
Dim ch As ChartObject, sbar As ScrollBar
Set ch = sheets(csheet).ChartObjects(1)
ch.Chart.SetSourceData Source:=sheets(csheet & "_AUX").Range(chtRng)
ch.Name = sn
Set sbar = sheets(csheet).ScrollBars(1)
sbar.Max = WorksheetFunction.Max(endRow - varArgs(3) - 1, 0)
sbar.Value = WorksheetFunction.Max(endRow - varArgs(3) - 1, 0)
End If
Exit Sub
EH_onChartChange:
MsgBox "Error(" & Err.Number & ") " & Err.Description & " [onChartChange]"
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 'drawChartTWH'를 호출할 때 추가된 인자로 차트명을 넘겨줍니다. 차트 생성이 아닌 전환의 경우, 이미 생성되어 있는 차트의 이름을 바꿉니다.
'CCandleFeeder'가 차트를 refresh하는 함수를 만들겠습니다. 그런데, 차트가 어느 시트에 있는지, 즉 차트출력 시트명이 무엇인지 알려주어야 합니다. 다음 그림과 같이 하단의 소스를 참고하여 멤버 변수를 추가하고 이를 초기화하는 함수를 입력합니다.
그림 8. 'CCandleFeeder'의 멤버 추가 |
클래스 소스 끝에 'refreshChart' 함수를 입력합니다. 하단 소스를 참고하십시요.
그림 9. 'refreshChart' 함수 작성 |
'CCandleFeeder'의 'store' 모듈이 추가된 'refreshChart'를 호출하도록 수정합니다. 하단 소스를 참고하십시요.
※ (수정 후 'CCandleFeeder' 소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private pStockCode As StringPrivate pCandleSheet As String
Private pDDEsheet As String
Private pCHTsheet As String
Property Let StockCode(v As String)
If v = Empty Or v = "" Then _
Err.Raise 8901, "CCandleFeeder.Let StockCode", "Invalid parameter"
pStockCode = v
End Property
Property Let CandleSheet(v As String)
If v = Empty Or v = "" Then _
Err.Raise 8901, "CCandleFeeder.Let CandleSheet", "Invalid parameter"
pCandleSheet = v
End Property
Property Let DDEsheet(v As String)
If v = Empty Or v = "" Then _
Err.Raise 8901, "CCandleFeeder.Let DDEsheet", "Invalid parameter"
pDDEsheet = v
End Property
Property Let CHTsheet(v As String)
If v = Empty Or v = "" Then _
Err.Raise 8901, "CCandleFeeder.Let CHTsheet", "Invalid parameter"
pCHTsheet = v
End Property
Sub store(s_code)
Static dde_cell As Integer 'the row of DDE cells of selected stock
Static prev_dt As String 'previous date
Static prev_tu As Integer 'previous time unit
Static prev_li As Integer 'row number of the row being updated
Static prev_vo As Double 'sum of volumes of prev candles of the same day
Static c_p_hr As Integer 'the number of candles per hour
On Error GoTo EH_storeCandleInfo:
If s_code = pStockCode Then
sh_name = pCandleSheet
'Debug.Print "sh_name " & sh_name
If dde_cell = 0 Then dde_cell = rowSearch(pDDEsheet, 2, "'" & s_code & "'")
If c_p_hr = 0 Then c_p_hr = candlesPerHour(sh_name)
'Debug.Print "c_p_hr " & c_p_hr
With Sheets(sh_name)
'Debug.Print "prev_li " & prev_li
If prev_li = 0 Then
If .Cells(2, 1).Value = "" Then
prev_li = 1
Else
'prev_li = .UsedRange.Cells(.Cells.Count).Row '===> Overflow
prev_li = .Cells(1, 1).End(xlDown).Row
End If
End If
'Debug.Print "prev_li " & prev_li
cp = Sheets(pDDEsheet).Cells(dde_cell, 2).Value '현재가
cv = Sheets(pDDEsheet).Cells(dde_cell, 3).Value '거래량
dv = Abs(Sheets(pDDEsheet).Cells(dde_cell, 4).Value) '체결량
bt = Sheets(pDDEsheet).Cells(dde_cell, 5).Value '체결시간
If prev_li = 1 Then
If c_p_hr = -1 Then GoTo DailyNewCandle:
NewCandle:
prev_tu = WorksheetFunction.Floor(TimeValue(Right(bt, 8)) _
* 24 * c_p_hr, 1)
Debug.Print "prev_tu " & prev_tu
HourlyNewCandle:
Dim hr As Integer, hrstr As String
hr = WorksheetFunction.Floor(prev_tu / c_p_hr, 1) 'prev_tu / c_p_hr
hrstr = hr
If hr < 10 Then hrstr = "0" & hr
If c_p_hr > 1 Then
mt = (prev_tu Mod c_p_hr) * (60 / c_p_hr)
If Len(mt) = 1 Then mt = "0" & mt
hr_mt = "-" & hrstr & ":" & mt & ":00"
ElseIf c_p_hr > 0 Then
'hr_mt = "-" & prev_tu & ":00:00"
hr_mt = "-" & hrstr & ":00:00" ' ◀ 버그 수정
End If
DailyNewCandle:
prev_dt = Replace(Format(Date, "yyyy/mm/dd"), "-", "/")
Debug.Print "prev_dt & hr_mt " & prev_dt & hr_mt
prev_li = prev_li + 1
Call fillCandle(sh_name, prev_li, prev_dt & hr_mt, cp, cv - prev_vo) 'dv)
'Exit Sub
GoTo ChartRefresh:
End If
If prev_dt = "" Then prev_dt = Left(.Cells(prev_li, 1).Value, 10)
If c_p_hr = -1 Then
If DateValue(prev_dt) <> Date Then
GoTo DailyNewCandle:
Else
Call updateCandle(sh_name, prev_li, cp, cv)
GoTo SkipChartRefreshing:
End If
Else
If prev_tu = 0 Then
Debug.Print "prev_li " & prev_li
Debug.Print Right(.Cells(prev_li, 1).Value, 8)
prev_tu = WorksheetFunction.Floor(TimeValue( _
Right(.Cells(prev_li, 1).Value, 8) _
) * 24 * c_p_hr, 1)
End If
If DateValue(prev_dt) <> Date Then
Debug.Print "Going to NewCandle"
GoTo NewCandle:
Else
cur_tu = WorksheetFunction.Floor(TimeValue(Right(bt, 8)) _
* 24 * c_p_hr, 1)
If prev_tu <> cur_tu Then
Debug.Print Right(bt, 8)
Debug.Print prev_tu & "," & cur_tu
prev_vo = cv - dv
prev_tu = cur_tu
GoTo HourlyNewCandle:
Else
If prev_vo = 0 Then prev_vo = cv - dv - .Cells(prev_li, 6).Value
Call updateCandle(sh_name, prev_li, cp, cv - prev_vo)
GoTo SkipChartRefreshing:
End If
End If
End If
End With
ChartRefresh:
Call refreshChart(pCHTsheet)
SkipChartRefreshing:
End If
Exit Sub
EH_storeCandleInfo:
'MsgBox "Error(" & Err.Number & ") " & Err.Description & " [store]"
Err.Raise Err.Number, "CCandleFeeder.store", Err.Description
End Sub
Private Function fillCandle(sh_name, l, d, p, v)
With Sheets(sh_name)
.Cells(l, 1).Value = d
.Cells(l, 2).Value = p
.Cells(l, 3).Value = p
.Cells(l, 4).Value = p
.Cells(l, 5).Value = p
.Cells(l, 6).Value = v
End With
End Function
Private Function updateCandle(sh_name, l, p, v)
With Sheets(sh_name)
If p > .Cells(l, 3).Value Then .Cells(l, 3).Value = p
If p < .Cells(l, 4).Value Then .Cells(l, 4).Value = p
.Cells(l, 5).Value = p
.Cells(l, 6).Value = v
End With
End Function
Private Function refreshChart(csheet)
Dim ch As ChartObject, sbar As ScrollBar
Debug.Print "Chart Refreshing..."
If Not sheetExist(csheet) Then GoTo SkipChartRefreshing:
On Error Resume Next
Set ch = Sheets(csheet).ChartObjects(1)
If Err.Number <> 0 Then
Err.Clear
GoTo SkipChartRefreshing:
End If
If Not ch.Name = pCandleSheet Then GoTo SkipChartRefreshing:
Set sbar = Sheets(csheet).ScrollBars(1)
sbar.Max = sbar.Max + 1
sbar.Value = sbar.Value + 1
Set ch = Nothing
Set sbar = Nothing
SkipChartRefreshing:
Debug.Print "End Chart Refreshing"
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 하이라이트 된 부분이 수정 또는 추가된 부분입니다.마지막으로 'CCandleFeeder'의 멤버가 추가되었으므로, 오브젝트를 생성하는 부분을 수정해야 합니다.
※ (소스)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub storeCandleInfo(s_code)Static feeders As Collection
'On Error GoTo EH_storeCandleInfo:
If feeders Is Nothing Then Set feeders = New Collection
For i = LBound(ss_list) To UBound(ss_list)
Dim cfeeder As CCandleFeeder
On Error Resume Next
Set cfeeder = feeders.Item(ss_list(i, 1))
If Err.Number = 5 Then GoTo RegisterFeeder:
On Error GoTo 0
If Not cfeeder Is Nothing Then GoTo DoFeed:
On Error GoTo EH_storeCandleInfo:
RegisterFeeder:
Set cfeeder = New CCandleFeeder
cfeeder.StockCode = ss_list(i, 1)
cfeeder.CandleSheet = ss_list(i, 2)
cfeeder.DDEsheet = dde_sheet
cfeeder.CHTsheet = CHART_POSTFIX
feeders.Add cfeeder, ss_list(i, 1)
DoFeed:
cfeeder.store (s_code)
NextLoop:
Next i
Exit Sub
EH_storeCandleInfo:
If Err.Number = 8901 Then
Debug.Print "Error(8901) " & Err.Source
Err.Clear
GoTo NextLoop:
End If
MsgBox "Error(" & Err.Number & ") " & Err.Description & " [storeCandleInfo]"
Err.Raise Err.Number, "storeCandleInfo", Err.Description
End Sub
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
☞ 하이라이트 된 라인만 추가하면 됩니다.
모든 작업을 완료했습니다. 차트가 제대로 refresh되는지 확인해 볼까요? ※ (주의) 'fillChartRng'함수의 사소한 버그를 수정했습니다. 지난 글에서 다시 복사하여 사용하시기 바랍니다.
※ 다음 글부터 '기술적 지표'와 관련된 내용을 진행합니다.
※ 다음 글부터 '기술적 지표'와 관련된 내용을 진행합니다.
댓글 없음:
댓글 쓰기