Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/kakao.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cmd

import "github.com/spf13/cobra"

var kakaoCmd = &cobra.Command{
Use: "kakao",
Short: "카카오톡 채널, 템플릿을 관리합니다",
}

func init() {
rootCmd.AddCommand(kakaoCmd)
}
13 changes: 13 additions & 0 deletions cmd/kakao_brand_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

import "github.com/spf13/cobra"

var kakaoBrandTemplateCmd = &cobra.Command{
Use: "brand-template",
Aliases: []string{"btpl"},
Short: "카카오 브랜드 템플릿을 관리합니다",
}

func init() {
kakaoCmd.AddCommand(kakaoBrandTemplateCmd)
}
147 changes: 147 additions & 0 deletions cmd/kakao_brand_template_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var (
kakaoBtplCreateFlagChannelID string
kakaoBtplCreateFlagChannelGroupID string
kakaoBtplCreateFlagChatBubbleType string
kakaoBtplCreateFlagName string
kakaoBtplCreateFlagContent string
kakaoBtplCreateFlagAdult bool
kakaoBtplCreateFlagHeader string
kakaoBtplCreateFlagImageID string
kakaoBtplCreateFlagImageLink string
kakaoBtplCreateFlagAdditional string
kakaoBtplCreateFlagCarousel string
kakaoBtplCreateFlagMainWideItem string
kakaoBtplCreateFlagSubWideItemList string
kakaoBtplCreateFlagVideo string
kakaoBtplCreateFlagCommerce string
kakaoBtplCreateFlagButtons string
kakaoBtplCreateFlagCoupon string
)

var kakaoBrandTemplateCreateCmd = &cobra.Command{
Use: "create",
Short: "브랜드 템플릿을 생성합니다",
Long: `브랜드 템플릿을 생성합니다. 검수 없이 즉시 사용 가능합니다.

사용법:
solactl kakao brand-template create --channel-id <channelId> --chat-bubble-type TEXT --content "내용"
solactl kakao brand-template create --channel-id <channelId> --chat-bubble-type IMAGE --content "내용" --image-id <imageId>`,
RunE: runKakaoBrandTemplateCreate,
}

func init() {
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagChannelID, "channel-id", "", "채널 ID (--channel-id 또는 --channel-group-id 필수)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagChannelGroupID, "channel-group-id", "", "채널 그룹 ID")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagChatBubbleType, "chat-bubble-type", "", "말풍선 타입 (필수: TEXT/IMAGE/WIDE/WIDE_ITEM_LIST/CAROUSEL_FEED/PREMIUM_VIDEO/COMMERCE/CAROUSEL_COMMERCE)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagName, "name", "", "이름")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagContent, "content", "", "내용")
kakaoBrandTemplateCreateCmd.Flags().BoolVar(&kakaoBtplCreateFlagAdult, "adult", false, "성인 콘텐츠")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagHeader, "header", "", "헤더")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagImageID, "image-id", "", "이미지 ID")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagImageLink, "image-link", "", "이미지 링크")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagAdditional, "additional-content", "", "추가 콘텐츠")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagCarousel, "carousel", "", "캐러셀 (JSON 객체)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagMainWideItem, "main-wide-item", "", "메인 와이드 아이템 (JSON 객체)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagSubWideItemList, "sub-wide-item-list", "", "서브 와이드 아이템 목록 (JSON 배열)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagVideo, "video", "", "동영상 (JSON 객체)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagCommerce, "commerce", "", "커머스 (JSON 객체)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagButtons, "buttons", "", "버튼 목록 (JSON 배열)")
kakaoBrandTemplateCreateCmd.Flags().StringVar(&kakaoBtplCreateFlagCoupon, "coupon", "", "쿠폰 (JSON 객체)")
kakaoBrandTemplateCmd.AddCommand(kakaoBrandTemplateCreateCmd)
}

func runKakaoBrandTemplateCreate(cmd *cobra.Command, args []string) error {
// XOR validation
if kakaoBtplCreateFlagChannelID == "" && kakaoBtplCreateFlagChannelGroupID == "" {
return fmt.Errorf("--channel-id 또는 --channel-group-id를 입력하세요")
}
if kakaoBtplCreateFlagChannelID != "" && kakaoBtplCreateFlagChannelGroupID != "" {
return fmt.Errorf("--channel-id와 --channel-group-id는 동시에 사용할 수 없습니다")
}
if kakaoBtplCreateFlagChatBubbleType == "" {
return fmt.Errorf("말풍선 타입(--chat-bubble-type)을 입력하세요")
}

c, err := newClient()
if err != nil {
return err
}

body := map[string]any{
"chatBubbleType": kakaoBtplCreateFlagChatBubbleType,
}

// brand-template API uses pfId/pfGroupId directly
if kakaoBtplCreateFlagChannelID != "" {
body["pfId"] = kakaoBtplCreateFlagChannelID
}
if kakaoBtplCreateFlagChannelGroupID != "" {
body["pfGroupId"] = kakaoBtplCreateFlagChannelGroupID
}

if kakaoBtplCreateFlagName != "" {
body["name"] = kakaoBtplCreateFlagName
}
if kakaoBtplCreateFlagContent != "" {
body["content"] = kakaoBtplCreateFlagContent
}
if kakaoBtplCreateFlagAdult {
body["adult"] = true
}
if kakaoBtplCreateFlagHeader != "" {
body["header"] = kakaoBtplCreateFlagHeader
}
if kakaoBtplCreateFlagImageID != "" {
body["imageId"] = kakaoBtplCreateFlagImageID
}
if kakaoBtplCreateFlagImageLink != "" {
body["imageLink"] = kakaoBtplCreateFlagImageLink
}
if kakaoBtplCreateFlagAdditional != "" {
body["additionalContent"] = kakaoBtplCreateFlagAdditional
}

// JSON fields
if err := setJSONField(body, "carousel", kakaoBtplCreateFlagCarousel); err != nil {
return err
}
if err := setJSONField(body, "mainWideItem", kakaoBtplCreateFlagMainWideItem); err != nil {
return err
}
if err := setJSONField(body, "subWideItemList", kakaoBtplCreateFlagSubWideItemList); err != nil {
return err
}
if err := setJSONField(body, "video", kakaoBtplCreateFlagVideo); err != nil {
return err
}
if err := setJSONField(body, "commerce", kakaoBtplCreateFlagCommerce); err != nil {
return err
}
if err := setJSONField(body, "buttons", kakaoBtplCreateFlagButtons); err != nil {
return err
}
if err := setJSONField(body, "coupon", kakaoBtplCreateFlagCoupon); err != nil {
return err
}

raw, err := c.Post(ctx(), "kakao/v2/brand-templates", body)
if err != nil {
return fmt.Errorf("브랜드 템플릿 생성 실패: %w", err)
}

p := printer()
if flagJSON {
return p.PrintJSON(raw)
}

_, _ = fmt.Fprintln(out(), "브랜드 템플릿이 생성되었습니다.")
return nil
}
64 changes: 64 additions & 0 deletions cmd/kakao_brand_template_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"bufio"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
)

var kakaoBtplDeleteFlagYes bool

var kakaoBrandTemplateDeleteCmd = &cobra.Command{
Use: "delete <brandTemplateId>",
Short: "브랜드 템플릿을 삭제합니다",
Long: `브랜드 템플릿을 삭제합니다.

사용법:
solactl kakao brand-template delete KA01BP240000000000000000000000000
solactl kakao brand-template delete KA01BP240000000000000000000000000 --yes`,
Args: cobra.ExactArgs(1),
RunE: runKakaoBrandTemplateDelete,
}

func init() {
kakaoBrandTemplateDeleteCmd.Flags().BoolVarP(&kakaoBtplDeleteFlagYes, "yes", "y", false, "확인 없이 삭제")
kakaoBrandTemplateCmd.AddCommand(kakaoBrandTemplateDeleteCmd)
}

func runKakaoBrandTemplateDelete(cmd *cobra.Command, args []string) error {
brandTemplateID := args[0]

if !kakaoBtplDeleteFlagYes {
_, _ = fmt.Fprintf(out(), "\u26a0 브랜드 템플릿 %s을 삭제하시겠습니까? (y/N): ", brandTemplateID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

stdinReader 변수가 cmd 패키지 내의 비-테스트 파일 어디에도 정의되어 있지 않습니다. 이로 인해 컴파일 오류("undefined: stdinReader")가 발생합니다. 테스트 시 입력을 모킹하기 위한 용도라면 cmd/root.go 등에 패키지 수준 변수로 선언해야 합니다.

reader := stdinReader
if reader == nil {
reader = bufio.NewReader(os.Stdin)
}
line, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("입력을 읽을 수 없습니다: %w", err)
}
answer := strings.TrimSpace(strings.ToLower(line))
if answer != "y" && answer != "yes" {
_, _ = fmt.Fprintln(out(), "삭제가 취소되었습니다.")
return nil
}
}

c, err := newClient()
if err != nil {
return err
}

_, err = c.Delete(ctx(), fmt.Sprintf("kakao/v2/brand-templates/%s", brandTemplateID))
if err != nil {
return fmt.Errorf("브랜드 템플릿 삭제 실패: %w", err)
}

_, _ = fmt.Fprintf(out(), "브랜드 템플릿 %s이 삭제되었습니다.\n", brandTemplateID)
return nil
}
114 changes: 114 additions & 0 deletions cmd/kakao_brand_template_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cmd

import (
"encoding/json"
"fmt"
"net/url"
"strconv"

"github.com/spf13/cobra"

"github.com/solapi/solactl/pkg/types"
)

var (
kakaoBtplListFlagName string
kakaoBtplListFlagChannelID string
kakaoBtplListFlagChannelGroupID string
kakaoBtplListFlagBrandTemplateID string
kakaoBtplListFlagChatBubbleType string
kakaoBtplListFlagStatus string
kakaoBtplListFlagStartKey string
kakaoBtplListFlagLimit int
)

var kakaoBrandTemplateListCmd = &cobra.Command{
Use: "list",
Short: "브랜드 템플릿 목록을 조회합니다",
Long: `브랜드 템플릿 목록을 조회합니다.

사용법:
solactl kakao brand-template list
solactl kakao brand-template list --chat-bubble-type TEXT
solactl kakao brand-template list --start-key <nextKey>`,
RunE: runKakaoBrandTemplateList,
}

func init() {
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagName, "name", "", "이름 필터")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagChannelID, "channel-id", "", "채널 ID 필터")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagChannelGroupID, "channel-group-id", "", "채널 그룹 ID 필터")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagBrandTemplateID, "brand-template-id", "", "브랜드 템플릿 ID 필터")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagChatBubbleType, "chat-bubble-type", "", "말풍선 타입 필터")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagStatus, "status", "", "상태 필터 (ACTIVE/INACTIVE/DELETED)")
kakaoBrandTemplateListCmd.Flags().StringVar(&kakaoBtplListFlagStartKey, "start-key", "", "페이지네이션 시작 키")
kakaoBrandTemplateListCmd.Flags().IntVar(&kakaoBtplListFlagLimit, "limit", 20, "조회 건수")
kakaoBrandTemplateCmd.AddCommand(kakaoBrandTemplateListCmd)
}

func runKakaoBrandTemplateList(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}

params := url.Values{}
params.Set("limit", strconv.Itoa(kakaoBtplListFlagLimit))
if kakaoBtplListFlagName != "" {
params.Set("name", kakaoBtplListFlagName)
}
// brand-template API uses pfId/pfGroupId directly
if kakaoBtplListFlagChannelID != "" {
params.Set("pfId", kakaoBtplListFlagChannelID)
}
if kakaoBtplListFlagChannelGroupID != "" {
params.Set("pfGroupId", kakaoBtplListFlagChannelGroupID)
}
if kakaoBtplListFlagBrandTemplateID != "" {
params.Set("brandTemplateId", kakaoBtplListFlagBrandTemplateID)
}
if kakaoBtplListFlagChatBubbleType != "" {
params.Set("chatBubbleType", kakaoBtplListFlagChatBubbleType)
}
if kakaoBtplListFlagStatus != "" {
params.Set("status", kakaoBtplListFlagStatus)
}
if kakaoBtplListFlagStartKey != "" {
params.Set("startKey", kakaoBtplListFlagStartKey)
}

raw, err := c.Get(ctx(), "kakao/v2/brand-templates", params)
if err != nil {
return fmt.Errorf("브랜드 템플릿 목록 조회 실패: %w", err)
}

p := printer()
if flagJSON {
return p.PrintJSON(raw)
}

var resp types.KakaoBrandTemplateListResponse
if err := json.Unmarshal(raw, &resp); err != nil {
return fmt.Errorf("응답 파싱 실패: %w", err)
}

headers := []string{"BRAND TEMPLATE ID", "NAME", "BUBBLE TYPE", "STATUS", "CREATED"}
rows := make([][]string, 0, len(resp.BrandTemplateList))
for i := range resp.BrandTemplateList {
bt := &resp.BrandTemplateList[i]
rows = append(rows, []string{
bt.BrandTemplateID,
bt.Name,
bt.ChatBubbleType,
types.DisplayStatus(bt.Status),
types.DisplayDate(bt.DateCreated),
})
}
p.FormatTable(headers, rows)

if resp.NextKey != "" {
_, _ = fmt.Fprintf(errOut(), "\n다음 페이지: solactl kakao brand-template list --start-key %s\n", resp.NextKey)
}

return nil
}
Loading