블로그 글을 프로그램으로 올리는 도구를 만들 때 글 자체보다 까다로운 게 이미지입니다. 본문 HTML에 <img src="내 PC 경로"> 를 넣을 수는 없으니, 먼저 이미지를 블로그 서버에 올려 주소를 받은 다음 그 주소로 태그를 만들어야 합니다. 아래 함수는 네이버 블로그 구 에디터에서 "사진 첨부"를 누를 때 브라우저가 보내는 요청을 그대로 따라 한 것입니다. 로그인 쿠키(Cookie)는 이미 확보되어 있다고 가정합니다.

전체 흐름은 다섯 단계입니다. 첨부 창을 열어 세션키를 받고, 그 세션키 주소로 이미지를 multipart 형식으로 올리고, 유해 이미지 검사를 거쳐, 저장 요청으로 실제 경로를 받고, 완료 요청으로 마무리합니다. 마지막에 받은 경로로 에디터가 쓰는 형식의 <img> 태그를 조립합니다.

광고

전체 코드

    Private Function upphoto(ByVal FilePath As String, ByVal href As String) As String
        Dim sqeid As String = "QE_" & UNIX_TIME() & "60998"
        Dim PostData As String
        Dim ImageBinary() As Byte
        Dim Header1 As String = "", Header2 As String = ""
        Dim BHeader1() As Byte, BHeader2() As Byte, Buffer() As Byte
        'Dim Size1 As Long, Size2 As Long, Size3 As Long
        'Dim FN As Integer
        'Dim PostData As String
        Dim boundary As String, Headers As String
        Dim T As String = ""
        Dim URL As String = "https://blog.naver.com/AttachPhotoWebtopForm.nhn?mode=base&blogId=" & _nhnID
        With CreateObject("WinHttp.WinHttpRequest.5.1")
            .Open("GET", URL)
            .SetRequestHeader("Accept", "text/html, application/xhtml+xml, */*")
            .SetRequestHeader("Accept-Language", "ko-KR")
            .SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko")
            .SetRequestHeader("Host", "blog.naver.com")
            .SetRequestHeader("Connection", "Keep-Alive")
            If Len(Cookie) Then .SetRequestHeader("Cookie", Cookie)
            .Send()
            .WaitForResponse() : Application.DoEvents()
            'T = System.Text.Encoding.Default.GetString(.ResponseBody)
            T = .ResponseText
        End With
        'Clipboard.SetText(T)
        Dim sessionKey As String = Split(Split(T, "sessionKey=")(1), "&")(0)
        Dim FileName As String
        FileName = Split(FilePath, "\")(UBound(Split(FilePath, "\")))
        FileName = Split(FileName, ".")(0)
        FileName = textEN(FileName) & "." & Split(FilePath, ".")(UBound(Split(FilePath, ".")))
        FileName = Replace$(FileName, "%", "")

        Dim image As System.Drawing.Image = SafeImageFromFile(FilePath)
        Dim FormetType As String = String.Empty
        If image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Tiff.Guid Then
            FormetType = "TIFF"
        ElseIf image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Gif.Guid Then
            FormetType = "GIF"
        ElseIf image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Jpeg.Guid Then
            FormetType = "JPG"
        ElseIf image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Bmp.Guid Then
            FormetType = "BMP"
        ElseIf image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Png.Guid Then
            FormetType = "PNG"
        ElseIf image.RawFormat.Guid = System.Drawing.Imaging.ImageFormat.Icon.Guid Then
            FormetType = "ICO"
        Else
            Throw New System.ArgumentException("Invalid File Type")
        End If
        image.Dispose()

        Header1 &= "-----------------------------66411509834719795442171685220" & vbCrLf
        Header1 &= "Content-Disposition: form-data; name=""image""; filename=""" & FileName & """" & vbCrLf
        Header1 &= "Content-Type: image/" & FormetType.ToLower & vbCrLf
        Header1 &= "" & vbCrLf
        '// 이미지데이터
        Header2 = vbCrLf
        Header2 &= "-----------------------------66411509834719795442171685220" & vbCrLf
        Header2 &= "Content-Disposition: form-data; name=""filename""" & vbCrLf
        Header2 &= "" & vbCrLf
        Header2 &= "_Pngtree_2 5d black monitor illustration_4751253.png" & vbCrLf
        Header2 &= "-----------------------------66411509834719795442171685220" & vbCrLf
        Header2 &= "Content-Disposition: form-data; name=""autorotate""" & vbCrLf
        Header2 &= "" & vbCrLf
        Header2 &= "true" & vbCrLf
        Header2 &= "-----------------------------66411509834719795442171685220" & vbCrLf
        Header2 &= "Content-Disposition: form-data; name=""extractAnimatedCnt""" & vbCrLf
        Header2 &= "" & vbCrLf
        Header2 &= "true" & vbCrLf
        Header2 &= "-----------------------------66411509834719795442171685220" & vbCrLf
        Header2 &= "Content-Disposition: form-data; name=""userId""" & vbCrLf
        Header2 &= "" & vbCrLf
        Header2 &= "" & _nhnID & "" & vbCrLf
        Header2 &= "-----------------------------66411509834719795442171685220--" & vbCrLf

        '### 이미지 바이너리를 얻는다.
        ImageBinary = IO.File.ReadAllBytes(FilePath)

        '### 전송할 데이터를 만든다.
        BHeader1 = System.Text.Encoding.UTF8.GetBytes(Header1)
        BHeader2 = System.Text.Encoding.UTF8.GetBytes(Header2)

        Dim DS As New MemoryStream
        DS.Write(BHeader1, 0, BHeader1.Length)
        DS.Write(ImageBinary, 0, ImageBinary.Length)
        DS.Write(BHeader2, 0, BHeader2.Length)

        DS.Position = 0
        ReDim Buffer(DS.Length)
        DS.Read(Buffer, 0, Buffer.Length) : DS.Close()

        URL = "https://blog.upphoto.naver.com/" & sessionKey & "/upload/0"
        With CreateObject("WinHttp.WinHttpRequest.5.1")
            .Open("POST", URL)
            .SetRequestHeader("Host", "blog.upphoto.naver.com")
            .SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0")
            .SetRequestHeader("Accept", "*/*")
            .SetRequestHeader("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3")
            .SetRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------66411509834719795442171685220")
            .SetRequestHeader("Content-Length", Buffer.Length)
            .SetRequestHeader("Origin", "https://blog.upphoto.naver.com")
            .SetRequestHeader("Connection", "keep-alive")
            .SetRequestHeader("Referer", "https://blog.upphoto.naver.com/" & sessionKey & "/startup?mode=base&width=740")
            If Len(Cookie) Then .SetRequestHeader("Cookie", Cookie)
            .Send(Buffer)
            .WaitForResponse() : Application.DoEvents()
            'T = System.Text.Encoding.Default.GetString(.ResponseBody)
            T = .ResponseText
        End With
        '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
        '<item>
        '    <index>0</index>
        '    <url>/" & sessionKey & "/0</url>
        '    <path>" & sessionKey & "</path>
        '    <fileName>0</fileName>
        '    <width>500</width>
        '    <height>476</height>

        '    <fileSize>23112</fileSize>
        '    <thumbnail>/" & sessionKey & "/thumbnail/0</thumbnail>

        '    <animatedCnt>1</animatedCnt>

        '</item>

        URL = "https://blog.upphoto.naver.com/" & sessionKey & "/checkHarm/0"
        With CreateObject("WinHttp.WinHttpRequest.5.1")
            .Open("GET", URL)
            .SetRequestHeader("Host", "blog.upphoto.naver.com")
            .SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0")
            .SetRequestHeader("Accept", "application/xml, text/xml, */*; q=0.01")
            .SetRequestHeader("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3")
            .SetRequestHeader("Connection", "keep-alive")
            .SetRequestHeader("Referer", "https://blog.upphoto.naver.com/" & sessionKey & "/startup?mode=base&width=740")
            If Len(Cookie) Then .SetRequestHeader("Cookie", Cookie)
            .Send()
            .WaitForResponse() : Application.DoEvents()
            'T = System.Text.Encoding.Default.GetString(.ResponseBody)
            T = .ResponseText
        End With
        '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
        '<result>
        '    <isSuccess>true</isSuccess>
        '    <isHarm>false</isHarm>
        '</result>

        PostData = "customKeyVersion=0"
        URL = "https://blog.upphoto.naver.com/" & sessionKey & "/save/0"
        With CreateObject("WinHttp.WinHttpRequest.5.1")
            .Open("POST", URL)
            .SetRequestHeader("Host", "blog.upphoto.naver.com")
            .SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0")
            .SetRequestHeader("Accept", "*/*")
            .SetRequestHeader("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3")
            .SetRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
            .SetRequestHeader("Content-Length", PostData.Length)
            .SetRequestHeader("Origin", "https://blog.upphoto.naver.com")
            .SetRequestHeader("Connection", "keep-alive")
            .SetRequestHeader("Referer", "https://blog.upphoto.naver.com/" & sessionKey & "/startup?mode=base&width=740")
            If Len(Cookie) Then .SetRequestHeader("Cookie", Cookie)
            .Send(PostData)
            .WaitForResponse() : Application.DoEvents()
            'T = System.Text.Encoding.Default.GetString(.ResponseBody)
            T = .ResponseText
        End With
        '  <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
        '- <item>
        '  <index>0</index>
        '  <url>/MjAyMDA1MjNfMTk5/MDAxNTkwMjA4MzkxODc5.AiNoZLJeWa2a5EhsbcdtPNyYXNy69o3TlMGERJt1ahog.8CBmBNL5qGsyh6eEZ9lMEDCabXiG7eMfrFs1Hpy75n0g.PNG.program1472/_Pngtree_2_5d_black_monitor_illustration_4751253.png</url>
        '  <path>/MjAyMDA1MjNfMTk5/MDAxNTkwMjA4MzkxODc5.AiNoZLJeWa2a5EhsbcdtPNyYXNy69o3TlMGERJt1ahog.8CBmBNL5qGsyh6eEZ9lMEDCabXiG7eMfrFs1Hpy75n0g.PNG.program1472</path>
        '  <fileName>_Pngtree_2_5d_black_monitor_illustration_4751253.png</fileName>
        '  <width>500</width>
        '  <height>476</height>
        '  <fileSize>23112</fileSize>
        '  <thumbnail>/MjAyMDA1MjNfMTk5/MDAxNTkwMjA4MzkxODc5.AiNoZLJeWa2a5EhsbcdtPNyYXNy69o3TlMGERJt1ahog.8CBmBNL5qGsyh6eEZ9lMEDCabXiG7eMfrFs1Hpy75n0g.PNG.program1472/_Pngtree_2_5d_black_monitor_illustration_4751253.png</thumbnail>
        '  </item>
        Dim width As Integer = Split(Split(T, "<width>")(1), "</width>")(0)
        Dim height As Integer = Split(Split(T, "<height>")(1), "</height>")(0)
        Dim thumbnail As String = Split(Split(T, "<thumbnail>")(1), "</thumbnail>")(0)

        URL = "https://blog.upphoto.naver.com/" & sessionKey & "/finish"
        With CreateObject("WinHttp.WinHttpRequest.5.1")
            .Open("GET", URL)
            .SetRequestHeader("Host", "blog.upphoto.naver.com")
            .SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0")
            .SetRequestHeader("Accept", "*/*")
            .SetRequestHeader("Accept-Language", "ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3")
            .SetRequestHeader("Connection", "keep-alive")
            .SetRequestHeader("Referer", "https://blog.upphoto.naver.com/" & sessionKey & "/startup?mode=base&width=740")
            If Len(Cookie) Then .SetRequestHeader("Cookie", Cookie)
            .Send()
            .WaitForResponse() : Application.DoEvents()
            'T = System.Text.Encoding.Default.GetString(.ResponseBody)
            T = .ResponseText
        End With
        '  <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
        '- <result>
        '  <isSuccess>true</isSuccess>
        '  </result>

        Dim HTML As String = ""
        If href.Length > 0 Then HTML &= "<a href=""" & href & """ target=""_self"">"
        HTML &= "<img id=""se_object_" & UNIX_TIME() & "5323"" "
        HTML &= "src=""https://blogfiles.pstatic.net" & thumbnail & """ "
        HTML &= "class=""__se_object"" "
        HTML &= "s_type=""attachment"" "
        HTML &= "s_subtype=""photo"" "
        HTML &= "rwidth=""" & width & """ "
        HTML &= "rheight=""" & height & """ "
        HTML &= "imgqe=""true"" "
        HTML &= "title=""" & thumbnail.Split("/"c).Last & """ "
        HTML &= "jsonvalue=""%7B%7D"" "
        HTML &= "style=""width: " & width & "px; height: " & height & "px; border-color: rgb(0, 0, 0);"" "
        HTML &= "sqeid=""" & sqeid & """ "
        HTML &= "width=""" & width & """ "
        HTML &= "height=""" & height & """ "
        HTML &= "align="""">"
        If href.Length > 0 Then HTML &= "</a>"
    End Function

UNIX_TIME, textEN, SafeImageFromFile 과 Cookie, _nhnID 는 원래 프로젝트에 있는 도우미 함수와 변수입니다. 각 요청 아래 주석은 그 단계에서 서버가 돌려준 XML 응답 예시라, 어떤 값을 잘라 쓰는지 볼 때 참고가 됩니다.

multipart 본문을 직접 조립하는 부분

이 함수에서 가장 손이 많이 가는 곳이 업로드 본문입니다. 파일 업로드는 multipart/form-data 형식이라, 경계 문자열(boundary)로 구분된 여러 필드를 바이트로 이어 붙여야 합니다. 코드는 이미지 앞부분 헤더(Header1), 이미지 바이트, 이미지 뒤 나머지 필드들(Header2)로 셋을 나눈 다음, 헤더는 UTF-8 바이트로 바꾸고 이미지는 File.ReadAllBytes 로 읽어 MemoryStream 에 차례로 씁니다. 문자열로 다 합쳐 버리면 이미지 바이트가 깨지기 때문에 이렇게 바이트 단위로 이어야 합니다.

Content-Type 헤더의 boundary는 본문의 구분선에서 앞의 -- 두 개를 뺀 값입니다. 본문에서 마지막 구분선 끝에 -- 가 붙는 것도 형식 규칙이고요. 이 짝이 하나라도 안 맞으면 서버는 파일을 못 찾았다고 응답합니다. 이미지 형식은 RawFormat 으로 실제 파일 내용을 확인해서 Content-Type 에 넣습니다. 확장자만 믿지 않는 이유는 확장자를 바꿔 저장한 파일이 의외로 많기 때문입니다.

그대로 쓰면 안 되는 부분

함수 끝에서 HTML 문자열을 다 만들어 놓고 Return HTML 이 빠져 있어서 실제로는 아무것도 돌려주지 않습니다. 마지막 줄 앞에 Return HTML 을 넣어야 합니다. 또 Header2 의 filename 필드에 테스트할 때 쓴 파일 이름(_Pngtree_2 5d black monitor illustration_4751253.png)이 고정으로 들어가 있습니다. 응답 예시를 보면 저장되는 파일 이름이 이 값을 따라가니, FileName 변수로 바꿔야 업로드한 파일 이름이 제대로 남습니다.

ReDim Buffer(DS.Length) 도 손볼 곳입니다. VB의 ReDim 은 마지막 인덱스를 받기 때문에 배열 길이가 스트림보다 1바이트 길어지고, 본문 끝에 0바이트가 하나 더 붙은 채 전송됩니다. 서버가 너그럽게 처리해서 동작은 했지만, Buffer = DS.ToArray() 로 바꾸는 게 정확합니다. 응답에서 값을 Split 으로 잘라 내는 부분도 응답이 예상과 다르면(로그인 만료 등) 인덱스 오류로 바로 죽으니, 실사용이라면 <isSuccess> 를 먼저 확인하는 방어 코드가 필요합니다.

마지막으로, 이 코드는 공개 API가 아니라 2020년 당시 블로그 에디터 화면이 내부적으로 쓰던 주소를 따라 한 것입니다. 네이버가 에디터를 바꾸면 주소와 필드가 통째로 달라지기 때문에, 지금 다시 쓰려면 브라우저 개발자 도구로 현재 요청을 다시 확인하고 맞춰야 합니다. 자동 업로드는 계정 제재 대상이 될 수 있다는 점도 염두에 두세요.