iCloud 기본 사항 및 코드 샘플 [닫힌]


85

초보자로서 저는 iCloud로 어려움을 겪고 있습니다. 몇 가지 샘플이 있지만 일반적으로 매우 자세합니다 (개발자 포럼에는 대규모 iCloud 및 CoreData 용 샘플이 있습니다). 사과 문서는 OK,하지만 난 여전히 큰 그림을 볼 수 없습니다. 그러니 참아주세요. 이러한 질문 중 일부는 매우 근본적이지만 대답하기 쉬울 수 있습니다.

컨텍스트 : 매우 간단한 iCloud 앱이 실행 중입니다 (아래 전체 샘플 코드). 사용자에게 표시되는 UITextView는 하나 뿐이며 입력 내용은 text.txt라는 파일에 저장됩니다.

여기에 이미지 설명 입력

txt 파일은 클라우드로 푸시되고 모든 장치에서 사용할 수 있습니다. 완벽하게 작동하지만 :

주요 문제 : iCloud를 사용하지 않는 사용자는 어떻습니까?

내 앱을 실행할 때 (아래 코드 참조) 사용자가 iCloud를 활성화했는지 확인합니다. iCloud가 활성화되어 있으면 모든 것이 정상입니다. 앱은 계속해서 클라우드에서 text.txt를 찾습니다. 발견되면로드하여 사용자에게 표시합니다. text.txt가 클라우드에서 발견되지 않으면 단순히 새 text.txt를 생성하여 사용자에게 표시합니다.

사용자가 iCloud를 활성화하지 않은 경우 아무 일도 일어나지 않습니다. 비 iCloud 사용자가 여전히 내 텍스트 앱으로 작업 할 수 있도록하려면 어떻게해야합니까? 아니면 그냥 무시합니까? 비 iCloud 사용자를 위해 별도의 함수를 작성해야합니까? 즉 단순히 문서 폴더에서 text.txt를로드하는 기능입니까?

애플은 다음과 같이 씁니다 .

앱 샌드 박스의 다른 모든 파일을 처리하는 것과 동일한 방식으로 iCloud의 파일을 처리합니다.

그러나 제 경우에는 더 이상 '일반적인'앱 샌드 박스가 없습니다. 클라우드에 있습니다. 아니면 항상 디스크에서 내 text.txt를 먼저로드 한 다음 iCloud에서 더 최신 정보가 있는지 확인합니까?

관련 문제 : 파일 구조-샌드 박스 대 클라우드

아마도 내 주요 문제는 iCloud가 작동하는 방식에 대한 근본적인 오해입니다. UIDocument의 새 인스턴스를 만들 때 두 가지 메서드를 덮어 써야합니다. 먼저 - (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError클라우드에서 파일을 가져온 다음 클라우드 -(id)contentsForType:(NSString *)typeName error:(NSError **)outError로 파일을 가져옵니다.

text.txt의 로컬 복사본을 샌드 박스에 저장하는 별도의 함수를 통합해야합니까? 비 iCloud 사용자에게도 작동합니까? iCloud를 이해하면 text.txt의 로컬 사본을 자동으로 저장합니다. 따라서 내 앱의 '오래된'샌드 박스에 아무것도 저장할 필요가 없어야합니다 (예 : 이전의 iCloud 시절에 있었던 것처럼). 현재 내 샌드 박스가 완전히 비어 있지만 이것이 올바른지 모르겠습니다. 거기에 text.txt의 다른 사본을 보관해야합니까? 이것은 내 데이터 구조를 복잡하게 만드는 것 같습니다 ... 클라우드에 하나의 text.txt가 있고, 하나는 내 기기의 iCloud 샌드 박스 (오프라인 상태에서도 작동 함)에 있고, 세 번째는 좋은 오래된 샌드 박스에 있습니다. 내 앱 ...


내 코드 : 간단한 iCloud 샘플 코드

이것은 개발자 포럼과 WWDC 세션 비디오에서 찾은 예제를 기반으로합니다. 나는 그것을 최소한으로 제거했습니다. 내 MVC 구조가 좋은지 잘 모르겠습니다. 모델이 이상적이지 않은 AppDelegate에 있습니다. 더 나은 서비스를 제공하기위한 모든 제안을 환영합니다.


편집 : 주요 질문을 추출하여 [여기]에 게시했습니다. 4


개요 :

개요

클라우드에서 text.txt를로드하는 가장 중요한 부분 :

//  AppDelegate.h
//  iCloudText

#import <UIKit/UIKit.h>

@class ViewController;
@class MyTextDocument;

@interface AppDelegate : UIResponder <UIApplicationDelegate> {
    NSMetadataQuery *_query;
}

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong, nonatomic) MyTextDocument *document;

@end

//  AppDelegate.m
//  iCloudText

#import "AppDelegate.h"
#import "MyTextDocument.h"
#import "ViewController.h"

@implementation AppDelegate

@synthesize window = _window;
@synthesize viewController = _viewController;
@synthesize document = _document;

- (void)dealloc
{
    [_window release];
    [_viewController release];
    [super dealloc];
}

- (void)loadData:(NSMetadataQuery *)query {

    // (4) iCloud: the heart of the load mechanism: if texts was found, open it and put it into _document; if not create it an then put it into _document

    if ([query resultCount] == 1) {
        // found the file in iCloud
        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:url];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc openWithCompletionHandler:^(BOOL success) {
            if (success) {
                NSLog(@"AppDelegate: existing document opened from iCloud");
            } else {
                NSLog(@"AppDelegate: existing document failed to open from iCloud");
            }
        }];
    } else {
        // Nothing in iCloud: create a container for file and give it URL
        NSLog(@"AppDelegate: ocument not found in iCloud.");

        NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
        NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:@"Documents"] URLByAppendingPathComponent:@"text.txt"];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:ubiquitousPackage];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc saveToURL:[doc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            NSLog(@"AppDelegate: new document save to iCloud");
            [doc openWithCompletionHandler:^(BOOL success) {
                NSLog(@"AppDelegate: new document opened from iCloud");
            }];
        }];
    }
}

- (void)queryDidFinishGathering:(NSNotification *)notification {

    // (3) if Query is finished, this will send the result (i.e. either it found our text.dat or it didn't) to the next function

    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [self loadData:query];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];
    _query = nil; // we're done with it
}

-(void)loadDocument {

    // (2) iCloud query: Looks if there exists a file called text.txt in the cloud

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    //SCOPE
    [query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
    //PREDICATE
    NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, @"text.txt"];
    [query setPredicate:pred];
    //FINISHED?
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
    [query startQuery];

}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"AppDelegate: app did finish launching");
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Override point for customization after application launch.
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPhone" bundle:nil] autorelease];
    } else {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPad" bundle:nil] autorelease];
    }

    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];

    // (1) iCloud: init

    NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    if (ubiq) {
        NSLog(@"AppDelegate: iCloud access!");
        [self loadDocument];
    } else {
        NSLog(@"AppDelegate: No iCloud access (either you are using simulator or, if you are on your phone, you should check settings");
    }


    return YES;
}

@end

UIDocument

//  MyTextDocument.h
//  iCloudText

#import <Foundation/Foundation.h>
#import "ViewController.h"

@interface MyTextDocument : UIDocument {

    NSString *documentText;
    id delegate;

}

@property (nonatomic, retain) NSString *documentText;
@property (nonatomic, assign) id delegate;

@end

//  MyTextDocument.m
//  iCloudText

#import "MyTextDocument.h"
#import "ViewController.h"

@implementation MyTextDocument

@synthesize documentText = _text;
@synthesize delegate = _delegate;

// ** READING **

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError
{
    NSLog(@"UIDocument: loadFromContents: state = %d, typeName=%@", self.documentState, typeName);

    if ([contents length] > 0) {
        self.documentText = [[NSString alloc] initWithBytes:[contents bytes] length:[contents length] encoding:NSUTF8StringEncoding];
    }
    else {
        self.documentText = @"";
    }

    NSLog(@"UIDocument: Loaded the following text from the cloud: %@", self.documentText);


    // update textView in delegate...
    if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
        [_delegate noteDocumentContentsUpdated:self];
    }

    return YES;

}

// ** WRITING **

-(id)contentsForType:(NSString *)typeName error:(NSError **)outError
{
    if ([self.documentText length] == 0) {
        self.documentText = @"New Note";
    }

    NSLog(@"UIDocument: Will save the following text in the cloud: %@", self.documentText);

    return [NSData dataWithBytes:[self.documentText UTF8String] length:[self.documentText length]];
}
@end

뷰 컨트롤러

//
//  ViewController.h
//  iCloudText

#import <UIKit/UIKit.h>

@class MyTextDocument;

@interface ViewController : UIViewController <UITextViewDelegate> {

    IBOutlet UITextView *textView;

}

@property (nonatomic, retain) UITextView *textView;
@property (strong, nonatomic) MyTextDocument *document;

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument;

@end

//  ViewController.m
//  iCloudText

#import "ViewController.h"
#import "MyTextDocument.h"

@implementation ViewController

@synthesize textView = _textView;
@synthesize document = _document;

-(IBAction)dismissKeyboard:(id)sender {

    [_textView resignFirstResponder];

}

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument
{
    NSLog(@"VC: noteDocumentsUpdated");
    _textView.text = noteDocument.documentText;
}

-(void)textViewDidChange:(UITextView *)theTextView {

     NSLog(@"VC: textViewDidChange");
    _document.documentText = theTextView.text;
    [_document updateChangeCount:UIDocumentChangeDone];

}

4
나는 이것을 몇 가지 질문으로 나누는 것이 좋습니다. 여기에 몇 가지 다른 질문이 묻혀 있고 여기있는 텍스트 벽에서 선택하기가 어렵습니다. 이 질문을 다시 iCloud를 사용하지 않는 사람들을 위해 무엇을해야하는지 묻는 것으로 되돌리고 다른 질문 (샘플 코드의 관련 부분 만 포함)을 별도의 질문으로 나누겠습니다. 좋은 질문이지만 분리되어야한다고 생각합니다.
Brad Larson

@BradLarson 귀하의 의견에 감사드립니다. 질문이 조금 헷갈 리면 미안하지만 주요 질문은 앱 샌드 박스 대 iCloud 샌드 박스 문제라고 생각합니다. 나는 무슨 일이 일어나고 있는지 알기 위해 전체 컨텍스트가 중요하다고 생각했기 때문에 전체 코드 (가장 짧은 iCloud 코드 예, btw)를 제공했습니다 ...하지만 다른 질문을 열어이 질문에 다시 연결할 수 있습니다. 더 큰 그림을 얻으십시오.
n.evermind

@BradLarson 좋아, 나는 여기에 새로운 질문을 열었다 : stackoverflow.com/questions/7798555/…
n.evermind

사람들은 여전히 핵심 데이터 및 아이 클라우드에 핸들을 얻으려고 노력은이 링크를 시도 ossh.com.au/design-and-technology/software-development/...
던컨 Groenewald

닫히면 안됩니다 이것은 실제로 제가 iCloud에서 본 더 건설적인 게시물 중 하나입니다.
Andrew Smith

답변:


22

방금 문서를 다시 읽었는데 일반적인 접근 방식이 잘못된 것 같습니다. 먼저 샌드 박스에 파일을 만든 다음 클라우드로 이동해야합니다. 즉, Apple은 항상 동일한 파일의 세 가지 버전을 가져야한다고 제안하는 것 같습니다. 하나는 내 앱 디렉토리에 하나는 내 기기의 iCloud 데몬 디렉토리에 하나는 구름:

앱은 로컬 파일 및 디렉토리에 대해 수행하는 것과 동일한 기술을 사용하여 iCloud에서 파일 및 디렉토리를 관리합니다. iCloud의 파일과 디렉토리는 여전히 파일과 디렉토리 일뿐입니다. 파일을 열고, 만들고, 이동하고, 복사하고, 읽고 쓰고, 삭제하거나, 수행 할 기타 작업을 수행 할 수 있습니다. 로컬 파일 및 디렉토리와 iCloud 파일 및 디렉토리의 유일한 차이점은 액세스하는 데 사용하는 URL입니다. 앱의 샌드 박스에 상대적인 URL 대신 iCloud 파일 및 디렉토리의 URL은 해당 iCloud 컨테이너 디렉토리에 상대적입니다.

파일 또는 디렉토리를 iCloud로 이동하려면 :

앱 샌드 박스에서 로컬로 파일 또는 디렉터리를 만듭니다. 사용하는 동안 파일 또는 디렉토리는 UIDocument 객체와 같은 파일 표시자가 관리해야합니다.

URLForUbiquityContainerIdentifier : 메소드를 사용하여 항목을 저장하려는 iCloud 컨테이너 디렉토리의 URL을 검색합니다. 컨테이너 디렉토리 URL을 사용하여 iCloud에서 항목의 위치를 ​​지정하는 새 URL을 만듭니다. NSFileManager의 setUbiquitous : itemAtURL : destinationURL : error : 메소드를 호출하여 항목을 iCloud로 이동합니다. 앱의 메인 스레드에서이 메서드를 호출하지 마십시오. 이렇게하면 메인 스레드가 장기간 차단되거나 앱의 자체 파일 발표자 중 하나와 교착 상태가 발생할 수 있습니다. 파일 또는 디렉토리를 iCloud로 이동하면 시스템은 해당 항목을 앱 샌드 박스에서 개인 로컬 디렉토리로 복사하여 iCloud 데몬에서 모니터링 할 수 있도록합니다. 파일이 더 이상 샌드 박스에 없더라도 앱은 여전히 ​​파일에 대한 전체 액세스 권한을 갖습니다. 파일의 사본은 현재 기기에 로컬로 남아 있지만 파일은 다른 기기에 배포 할 수 있도록 iCloud로도 전송됩니다. iCloud 데몬은 로컬 복사본이 동일한 지 확인하는 모든 작업을 처리합니다. 따라서 앱의 관점에서 파일은 iCloud에 있습니다.

iCloud의 파일 또는 디렉토리에 대한 모든 변경 사항은 파일 조정자 객체를 사용하여 이루어져야합니다. 이러한 변경에는 항목 이동, 삭제, 복사 또는 이름 변경이 포함됩니다. 파일 코디네이터는 iCloud 데몬이 파일이나 디렉토리를 동시에 변경하지 않도록하고 다른 이해 당사자에게 변경 사항을 알려줍니다.

그러나 setUbiquitous와 관련된 문서를 조금 더 자세히 살펴보면 다음을 찾을 수 있습니다.

이 방법을 사용하여 파일을 현재 위치에서 iCloud로 이동합니다. 응용 프로그램의 샌드 박스에 있는 파일의 경우 샌드 박스 디렉토리에서 파일을 물리적으로 제거해야합니다 . (시스템은 응용 프로그램의 샌드 박스 권한을 확장하여 iCloud로 이동하는 파일에 대한 접근 권한을 부여합니다.) 또한이 방법을 사용하여 파일을 iCloud에서 로컬 디렉토리로 이동할 수 있습니다.

따라서 이것은 파일 / 디렉토리가 로컬 샌드 박스에서 삭제되고 클라우드로 이동됨을 의미하는 것으로 보입니다.


1
URL 링크는 ... 고장
NGB

5

나는 당신의 예를 사용하고 있으며 iCloud의 기본 사항을 이해하는 데 도움이되는 것이 좋습니다. 이제 나는 내가 말할 수있는 한 iCloud를 사용하거나 사용하지 않을 수있는 로컬에 저장된 콘텐츠로 앱의 기존 사용자를 지원 해야하는 내 앱에 대한 질문을 제기하고 있습니다.

사례 :

  1. 새로운 사용자
    • icloud-icloud에서 문서 생성
    • icloud 없음-로컬로 문서 생성
  2. 기존 사용자
    • icloud 있음
      • 방금 추가됨-로컬 문서를 icloud로 마이그레이션
      • 추가되지 않음-icloud에 문서 열기 / 저장
    • icloud 없음
      • 방금 제거됨-이전 icloud 문서를 로컬로 마이그레이션
      • 제거되지 않음-로컬에 문서 열기 / 저장

누군가 iCloud를 제거하면 유비쿼터스 URL 호출이 nil을 반환하지 않습니까? 이 경우 문서를 로컬 저장소로 다시 마이그레이션하려면 어떻게해야합니까? 지금은 사용자 환경 설정을 만들 것이지만 약간의 해결 방법 인 것 같습니다.

나는 여기에 분명한 것이 놓친 것 같아서 누구든지 볼 수 있다면 차임하십시오.


이러한 경우를 처리하는 클래스가 있는지 궁금하다는 점을 추가해야하므로 그냥 사용하고 저장할 위치에 대해 걱정할 필요가 없습니다.
earnshavian

developer.apple.com/library/ios/#documentation/DataManagement/… 를 살펴보면 로컬 샌드 박스 또는 클라우드에 무엇을 넣어야하는지 결정하는 샘플 코드가 있습니다.
n.evermind

감사합니다. 나는 그 문서를 보았지만 iCloud 임무 초기에 제공하는 코드를 잊어 버렸습니다. 로컬 및 원격을 지원하도록 샘플을 조정하려고합니다. 유비쿼터스 URL을 잃어 버렸기 때문에 iCloud를 비활성화 한 사용자를 어떻게 처리하는지 아직 명확하지 않지만 균열이 생기고 업데이트를 공유 할 것입니다.
earnshavian

1
따라서 어떤면에서 클라우드 용 URL과 로컬 샌드 박스 용 PATH를 사용해야한다는 것은 약간 어리석은 일입니다. iCloud가 우리를 위해 모든 것을 처리 할 수 ​​있다면 좋을 것입니다.하지만 이렇게하면 기본적으로 우리가 여는 모든 파일에 대해 두 가지 다른 방법을 코딩해야합니다.
n.evermind

귀하의 게시물을 다시 읽었습니다. 이제 NSUserDefaults에 사용자의 기본 설정 (즉, 사용자가 iCloud를 사용하기 원하거나 원하지 않음)을 저장하고 있습니다. 이것이 Apple이 제안하는 것이기도합니다. iCloud에 액세스 할 수 있는지 항상 확인합니다. 액세스 할 수없는 경우 사용자에게 사용하도록 지시하지만 앱에 사용하고 싶지 않다고 명시 적으로 말하지 않은 경우에만 사용합니다. 그렇지 않으면 iCloud를 사용하지 않으려는 사람들에게 짜증이납니다. iCloud가 활성화되어 있는지 확인한 후에는 유비쿼터스 URL 경로를 따르고 UIDocument를 사용하거나 옛날처럼 단순히 샌드 박스에서 파일을 엽니 다.
n.evermind

4

사용자가 iOS 5.0 이전 기기간에 텍스트를 공유 할 수 있도록하려면 iCloud 이전에 모든 사람이해야했던 작업을 수행하고 정보를 자신의 서버로 이동해야합니다.

정말 필요한 것은 앱이 텍스트 파일을 저장하고 사용자 계정과 연결할 수있는 서버뿐입니다.

계정을 생성하려면 사용자가 필요하며 한 장치의 새 정보를 자신의 '클라우드'로 이동하는 프로세스를 직접 관리해야합니다.

사용자는 다른 장치에서 동일한 계정으로 등록하고 다른 장치가 데이터를 자신의 클라우드로 이동했을 때 감지하고 현재 장치를 새 정보로 업데이트해야합니다.

분명히 iOS 5.0 기기의 경우 자신의 클라우드에서 iOS 5.0 이전 기기의 변경된 파일을 감지하고 iCloud와 대화 할 수도 있습니다.


감사. 즉, iOS 5 이전 장치를 지원하지 않으려면 UIDocument로 이동하고 내 앱의 샌드 박스에있는 doc 디렉터리의 내용을 잊어 버립니다.
n.evermind

내가 말할 수있는 한, UIDocument가 iCloud를 조정하는 데 도움이 될 문서가 샌드 박스에 여전히 남아 있지만 액세스 할 수있을 때 알려줄 것입니다. 이 물건을 직접 잡으려고!
Jonathan Watmough 2011 년

3

iOS5 / notIOS5 문제만큼 iCloud / notICloud 문제로 어려움을 겪고있는 것 같지 않습니다.

배포 대상이 iOS5이면 항상 UIDocument 구조를 사용하면됩니다. 유비쿼터스 인 경우 NSMetaDataQuery가 클라우드에서 찾을 수 있습니다. 그렇지 않으면 장치에서 찾을 수 있습니다.

반면에 앱에 5.0 이전 액세스를 제공하려면 실행중인 iOS가 5.0 이상인지 조건부로 확인해야합니다. 그렇다면 UIDocument를 사용하십시오. 그렇지 않은 경우 이전 방식으로 데이터를 읽고 씁니다.

내 접근 방식은 iOS5를 확인하는 조건부 saveData 메서드를 작성하는 것이 었습니다. 존재하는 경우 변경 횟수를 업데이트하거나 실행 취소 관리자를 사용합니다. 귀하의 경우 textViewDidChange는이 메서드를 호출합니다. 그렇지 않은 경우 이전 방식으로 디스크에 저장합니다. 로딩시 그 반대가 발생합니다.


1

"앱 샌드 박스에있는 다른 모든 파일을 처리하는 것과 동일한 방식으로 iCloud에있는 파일을 처리합니다." 이것은 많은 파일을 보관하는 Keynote 및 Numbers와 같은 경우에 해당되며 iCloud가 있으면 마술처럼 동기화되기 시작합니다.

그러나 iCloud와 유사한 기능에 의존하는 무언가를 만들고 있습니다. 앱이 의도 한대로 작동하기 위해 iCloud에 의존하기 때문에이 진술을 고수 할 수 없습니다. 앱을 종료하고 "이 기능이 작동하려면 iCloud를 설정하십시오"라고 말하거나 상관없이 항상 사용할 수있는 iCloud와 유사한 기능 (사용자 또는 다른 사람의 기능)을 복제해야합니다.


감사. 그래서 나는 내가 iCloud 전용 앱을할지 아니면 iCloud 기능을 사용하는 사람들을 위해 일종의 하이브리드를할지 선택해야한다고 생각한다. iCloud가 너무 복잡하기 때문에 iCloud 전용 앱을 사용하는 경향이 있습니다. 감사.
n.evermind
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.