Zend Framework教程之动作的基类Zend_Controller_Action详解
本文实例讲述了Zend Framework教程之动作的基类Zend_Controller_Action。分享给大家供大家参考,具体如下: Zend_Controller_Action的实现 Zend Framework的动作控制器需要继承Zend_Controller_Action,Zend_Controller_Action提供了动作控制器的基本功能,
本文实例讲述了Zend Framework教程之动作的基类Zend_Controller_Action。分享给大家供大家参考,具体如下:
Zend_Controller_Action的实现
Zend Framework的动作控制器需要继承Zend_Controller_Action,Zend_Controller_Action提供了动作控制器的基本功能,具体参考如下代码:
Zend_Controller_Action_Interface
<?php interface Zend_Controller_Action_Interface { /** * Class constructor * * The request and response objects should be registered with the * controller, as should be any additional optional arguments; these will be * available via {@link getRequest()}, {@link getResponse()}, and * {@link getInvokeArgs()}, respectively. * * When overriding the constructor, please consider this usage as a best * practice and ensure that each is registered appropriately; the easiest * way to do so is to simply call parent::__construct($request, $response, * $invokeArgs). * * After the request, response, and invokeArgs are set, the * {@link $_helper helper broker} is initialized. * * Finally, {@link init()} is called as the final action of * instantiation, and may be safely overridden to perform initialization * tasks; as a general rule, override {@link init()} instead of the * constructor to customize an action controller's instantiation. * * @param Zend_Controller_Request_Abstract $request * @param Zend_Controller_Response_Abstract $response * @param array $invokeArgs Any additional invocation arguments * @return void */ public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array()); /** * Dispatch the requested action * * @param string $action Method name of action * @return void */ public function dispatch($action); }
Zend_Controller_Action
<?php require_once 'Zend/Controller/Action/HelperBroker.php'; require_once 'Zend/Controller/Action/Interface.php'; require_once 'Zend/Controller/Front.php'; abstract class Zend_Controller_Action implements Zend_Controller_Action_Interface { protected $_classMethods; protected $_delimiters; protected $_invokeArgs = array(); protected $_frontController; protected $_request = null; protected $_response = null; public $viewSuffix = 'phtml'; public $view; protected $_helper = null; public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array()) { $this->setRequest($request) ->setResponse($response) ->_setInvokeArgs($invokeArgs); $this->_helper = new Zend_Controller_Action_HelperBroker($this); $this->init(); } public function init() { } public function initView() { if (!$this->getInvokeArg('noViewRenderer') && $this->_helper->hasHelper('viewRenderer')) { return $this->view; } require_once 'Zend/View/Interface.php'; if (isset($this->view) && ($this->view instanceof Zend_View_Interface)) { return $this->view; } $request = $this->getRequest(); $module = $request->getModuleName(); $dirs = $this->getFrontController()->getControllerDirectory(); if (empty($module) || !isset($dirs[$module])) { $module = $this->getFrontController()->getDispatcher()->getDefaultModule(); } $baseDir = dirname($dirs[$module]) . DIRECTORY_SEPARATOR . 'views'; if (!file_exists($baseDir) || !is_dir($baseDir)) { require_once 'Zend/Controller/Exception.php'; throw new Zend_Controller_Exception('Missing base view directory ("' . $baseDir . '")'); } require_once 'Zend/View.php'; $this->view = new Zend_View(array('basePath' => $baseDir)); return $this->view; } public function render($action = null, $name = null, $noController = false) { if (!$this->getInvokeArg('noViewRenderer') && $this->_helper->hasHelper('viewRenderer')) { return $this->_helper->viewRenderer->render($action, $name, $noController); } $view = $this->initView(); $script = $this->getViewScript($action, $noController); $this->getResponse()->appendBody( $view->render($script), $name ); } public function renderScript($script, $name = null) { if (!$this->getInvokeArg('noViewRenderer') && $this->_helper->hasHelper('viewRenderer')) { return $this->_helper->viewRenderer->renderScript($script, $name); } $view = $this->initView(); $this->getResponse()->appendBody( $view->render($script), $name ); } public function getViewScript($action = null, $noController = null) { if (!$this->getInvokeArg('noViewRenderer') && $this->_helper->hasHelper('viewRenderer')) { $viewRenderer = $this->_helper->getHelper('viewRenderer'); if (null !== $noController) { $viewRenderer->setNoController($noController); } return $viewRenderer->getViewScript($action); } $request = $this->getRequest(); if (null === $action) { $action = $request->getActionName(); } elseif (!is_string($action)) { require_once 'Zend/Controller/Exception.php'; throw new Zend_Controller_Exception('Invalid action specifier for view render'); } if (null === $this->_delimiters) { $dispatcher = Zend_Controller_Front::getInstance()->getDispatcher(); $wordDelimiters = $dispatcher->getWordDelimiter(); $pathDelimiters = $dispatcher->getPathDelimiter(); $this->_delimiters = array_unique(array_merge($wordDelimiters, (array) $pathDelimiters)); } $action = str_replace($this->_delimiters, '-', $action); $script = $action . '.' . $this->viewSuffix; if (!$noController) { $controller = $request->getControllerName(); $controller = str_replace($this->_delimiters, '-', $controller); $script = $controller . DIRECTORY_SEPARATOR . $script; } return $script; } public function getRequest() { return $this->_request; } public function setRequest(Zend_Controller_Request_Abstract $request) { $this->_request = $request; return $this; } public function getResponse() { return $this->_response; } public function setResponse(Zend_Controller_Response_Abstract $response) { $this->_response = $response; return $this; } protected function _setInvokeArgs(array $args = array()) { $this->_invokeArgs = $args; return $this; } public function getInvokeArgs() { return $this->_invokeArgs; } public function getInvokeArg($key) { if (isset($this->_invokeArgs[$key])) { return $this->_invokeArgs[$key]; } return null; } public function getHelper($helperName) { return $this->_helper->{$helperName}; } public function getHelperCopy($helperName) { return clone $this->_helper->{$helperName}; } public function setFrontController(Zend_Controller_Front $front) { $this->_frontController = $front; return $this; } public function getFrontController() { // Used cache version if found if (null !== $this->_frontController) { return $this->_frontController; } // Grab singleton instance, if class has been loaded if (class_exists('Zend_Controller_Front')) { $this->_frontController = Zend_Controller_Front::getInstance(); return $this->_frontController; } // Throw exception in all other cases require_once 'Zend/Controller/Exception.php'; throw new Zend_Controller_Exception('Front controller class has not been loaded'); } public function preDispatch() { } public function postDispatch() { } public function __call($methodName, $args) { require_once 'Zend/Controller/Action/Exception.php'; if ('Action' == substr($methodName, -6)) { $action = substr($methodName, 0, strlen($methodName) - 6); throw new Zend_Controller_Action_Exception(sprintf('Action "%s" does not exist and was not trapped in __call()', $action), 404); } throw new Zend_Controller_Action_Exception(sprintf('Method "%s" does not exist and was not trapped in __call()', $methodName), 500); } public function dispatch($action) { // Notify helpers of action preDispatch state $this->_helper->notifyPreDispatch(); $this->preDispatch(); if ($this->getRequest()->isDispatched()) { if (null === $this->_classMethods) { $this->_classMethods = get_class_methods($this); } // If pre-dispatch hooks introduced a redirect then stop dispatch // @see ZF-7496 if (!($this->getResponse()->isRedirect())) { // preDispatch() didn't change the action, so we can continue if ($this->getInvokeArg('useCaseSensitiveActions') || in_array($action, $this->_classMethods)) { if ($this->getInvokeArg('useCaseSensitiveActions')) { trigger_error('Using case sensitive actions without word separators is deprecated; please do not rely on this "feature"'); } $this->$action(); } else { $this->__call($action, array()); } } $this->postDispatch(); } // whats actually important here is that this action controller is // shutting down, regardless of dispatching; notify the helpers of this // state $this->_helper->notifyPostDispatch(); } public function run(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null) { if (null !== $request) { $this->setRequest($request); } else { $request = $this->getRequest(); } if (null !== $response) { $this->setResponse($response); } $action = $request->getActionName(); if (empty($action)) { $action = 'index'; } $action = $action . 'Action'; $request->setDispatched(true); $this->dispatch($action); return $this->getResponse(); } protected function _getParam($paramName, $default = null) { $value = $this->getRequest()->getParam($paramName); if ((null === $value || '' === $value) && (null !== $default)) { $value = $default; } return $value; } protected function _setParam($paramName, $value) { $this->getRequest()->setParam($paramName, $value); return $this; } protected function _hasParam($paramName) { return null !== $this->getRequest()->getParam($paramName); } protected function _getAllParams() { return $this->getRequest()->getParams(); } final protected function _forward($action, $controller = null, $module = null, array $params = null) { $request = $this->getRequest(); if (null !== $params) { $request->setParams($params); } if (null !== $controller) { $request->setControllerName($controller); // Module should only be reset if controller has been specified if (null !== $module) { $request->setModuleName($module); } } $request->setActionName($action) ->setDispatched(false); } protected function _redirect($url, array $options = array()) { $this->_helper->redirector->gotoUrl($url, $options); } }
Zend_Controller_Action提供了动作和视图的render功能,以及注册请求和响应对象,常用助手等等。
动作控制器的常用方法
在动作控制器中常用的方法和属性如下:
$this->_helper主要完成助手的相关操作例如:
// 只是局部控制器;当初始化加载时,对这个控制器的所有动作有效: $this->_helper->viewRenderer->setNoRender(true); // 全局: $this->_helper->removeHelper('viewRenderer'); // 也是全局,但需要和本地版本协作,以便繁殖这个控制器: Zend_Controller_Front::getInstance()->setParam('noViewRenderer', true);
通过设置ViewRenderer的noRender标记,可以简单地为一个独立的视图禁止解析(rendering):
class FooController extends Zend_Controller_Action { public function barAction() { // disable autorendering for this action only: $this->_helper->viewRenderer->setNoRender(); } }
禁止ViewRenderer的主要原因是如果你不需要视图对象或者如果你不通过视图脚本(例如,当使用动作控制器来司服网站服务协议如SOAP,XML-RPC或REST)来解析。大多数情况下,你不需要全局地禁止ViewRenderer,只选择性地在个别控制器或动作里禁止它。
请求对象和响应对象的相关操作
无数的对象和变量与对象一起注册,并且每个都有访问器方法。
请求对象:getRequest()可用来读取调用动作请求对象。
响应对象: getResponse()可用来读取收集最终响应的响应对象。一些典型的调用看起来象这样:
$this->getResponse()->setHeader('Content-Type', 'text/xml'); $this->getResponse()->appendBody($content);
调用参数:前端控制器可能把参数传给路由器、派遣器和动作控制器。为了读取这些参数,可使用getInvokeArg($key);另外,用getInvokeArgs()读取整个参数列表。
请求参数:请求对象手机请求参数,如任何_GET 或 _POST 参数,或者指定在URL的路径信息里的用户参数。为了读取这些参数,可使用_getParam($key)或_getAllParams()。也可以用_setParam()来设置请求参数;当转发到另外的动作时这很有用。
用_hasParam($key)来测试是否一个参数存在(对逻辑分支有用)。
Note: _getParam()可带有一个可选的第二个参数,如果它不是空的,就包含一个缺省的值。用它在读取值之前来消除对_hasParam() 的调用:
// Use default value of 1 if id is not set $id = $this->_getParam('id', 1); // Instead of: if ($this->_hasParam('id') { $id = $this->_getParam('id'); } else { $id = 1; }
视图的相关操作
Zend_Controller_Action为视图继承提供了一个初步的灵活的机制。有两个方法来完成这个:initView() 和 render();前者松散地加载$view public 属性,后者基于当前请求的动作来解析视图,它们使用目录层次来决定脚本路径。
视图初始化
initView()初始化视图对象。为了读取视图对象,render()调用initView(),但它可以在任何时候被初始化;缺省地,它用Zend_View对象来组装$view属性,但任何实现Zend_View_Interface的类可以使用。如果$view已经被初始化,它就简单地返回属性。
缺省的实现使用下面假设的目录结构:
applicationOrModule/
controllers/
IndexController.php
views/
scripts/
index/
index.phtml
helpers/
filters/
换句话说,视图脚本假定放在views/scripts/子目录,同时假定 views子目录还包含兄弟功能(助手和过滤器)。确定视图脚本名称和路径时,先以 views/scripts/作为基路径,然后加上以视图脚本对应控制器命名的目录。
解析(Rendering)视图
render() 有下列特征:has the following signature:
string render(string $action = null, string $name = null, bool $noController = false);
render()解析视图脚本。如果没有传递参数,它假定请求的脚本是[controller]/[action].phtml (.phtml是$viewSuffix属性的值)。为$action传递一个值将解析在[controller]子目录中的模板。为用[controller]重写,传递一个true值给$noController。最后,模板被解析到响应对象;如果你希望解析到一个在响应对象里指定的named segment,传递一个值给$name。
Note: 因为控制器和动作名字里可能包含分隔符如'_'、 '.' 和 '-',当决定视图名字时,render()把它们规格化成 '-'.在内部,它使用派遣器的字和路径分隔符来做规格化。这样,对/foo.bar/baz-bat的请求将解析脚本foo-bar/baz-bat.phtml。如果动作方法包含camelCasing,记住当决定视图脚本文件名的时候,这将变成由'-'分隔的字。
一些例子:
class MyController extends Zend_Controller_Action { public function fooAction() { // Renders my/foo.phtml $this->render(); // Renders my/bar.phtml $this->render('bar'); // Renders baz.phtml $this->render('baz', null, true); // Renders my/login.phtml to the 'form' segment of the // response object $this->render('login', 'form'); // Renders site.phtml to the 'page' segment of the response // object; does not use the 'my/' subirectory $this->render('site', 'page', true); } public function bazBatAction() { // Renders my/baz-bat.phtml $this->render(); } }
其它
_forward($action, $controller = null, $module = null, array $params = null) :执行另外一个动作。如果在preDispatch()里调用,当前请求的动作将被跳过来支持新的动作。否则,在当前动作被处理之后,在_forward()请求的动作将被执行。
_redirect($url, array $options = array()):重定向到另外一个地方。这个方法用URL和一组可选的选项。缺省地,它执行HTTP 302 重定向。
选项可包括一个或多个下面这些:
exit:是否立即退出。如果被请求,它将干净地关闭任何打开的会话和执行重定向。
可以用setRedirectExit()访问器在控制器里全局地设置这个选项。
prependBase:是否预先考虑基础URL和URL提供的请求对象一起注册。
使用setRedirectPrependBase()访问器,在控制器里全局地设置这个选项。
code:在重定向时要用什么HTTP代码。缺省使用302;可以用从301到306之间的任何代码。
使用setRedirectCode()访问器,在控制器里全局地设置这个选项。
扩展自定义Zend_Controller_Action
为了创建动作控制器,设计上,Zend_Controller_Action 必须被继承。至少,需要定义控制器可能调用的动作方法。
除了为web应用程序创建有用的函数外,你可能发现在不同的控制器里重复同样的设置和实用方法;如果这样,创建一个继承(extends)Zend_Controller_Action 的基础类可能会解决问题。
Example #1 如何处理不存在的动作
如果控制器的请求包括一个未定义的动作方法,Zend_Controller_Action::__call()将被调用。__call()当然是PHP中用来重载方法的魔术方法。
缺省地,这个方法抛出一个Zend_Controller_Action_Exception 来表明在控制器里没有发现要求的方法。如果要求的方法以'Action'结尾,就假设一个动作被请求并且不存在;这样的错误导致带有代码为 404 的异常。所有其它方法导致带有代码为 500 的异常。这使你很容易地在错误句柄里区分是页面没有发现还是程序错误。
如果想执行其它操作,你应该重写这个函数。例如,如果你想显示错误信息,可以象下面这样来写:
class MyController extends Zend_Controller_Action { public function __call($method, $args) { if ('Action' == substr($method, -6)) { // If the action method was not found, render the error // template return $this->render('error'); } // all other methods throw an exception throw new Exception('Invalid method "' . $method . '" called', 500); } }
另外的可能性就是你可能想转发到缺省控制页面:
class MyController extends Zend_Controller_Action { public function indexAction() { $this->render(); } public function __call($method, $args) { if ('Action' == substr($method, -6)) { // If the action method was not found, forward to the // index action return $this->_forward('index'); } // all other methods throw an exception throw new Exception('Invalid method "' . $method . '" called', 500); } }
为了定制控制器,除了重写__call()以外,本章前面说涉及的初始化、实用程序、访问器、视图和派遣钩子等方法都可以被重写。作为例子,如果把视图对象保存到注册表里,你可能想用象下面的代码来修改initView():
abstract class My_Base_Controller extends Zend_Controller_Action { public function initView() { if (null === $this->view) { if (Zend_Registry::isRegistered('view')) { $this->view = Zend_Registry::get('view'); } else { $this->view = new Zend_View(); $this->view->setBasePath(dirname(__FILE__) . '/../views'); } } return $this->view; } }
更多关于zend相关内容感兴趣的读者可查看本站专题:《Zend FrameWork框架入门教程》、《php优秀开发框架总结》、《Yii框架入门及常用技巧总结》、《ThinkPHP入门教程》、《php面向对象程序设计入门教程》、《php+mysql数据库操作入门教程》及《php常见数据库操作技巧汇总》
希望本文所述对大家PHP程序设计有所帮助。

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

AI Hentai Generator
AI Hentai를 무료로 생성하십시오.

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











Dewu APP는 현재 매우 인기 있는 브랜드 쇼핑 소프트웨어이지만 대부분의 사용자는 Dewu APP의 기능을 사용하는 방법을 모릅니다. 다음으로 편집기는 Dewuduo를 사용자에게 제공합니다. 관심 있는 사용자는 와서 살펴볼 수 있습니다! Dewu 이용방법 튜토리얼 [2024-03-20] Dewu 할부구매 이용방법 [2024-03-20] Dewu 쿠폰 받는 방법 [2024-03-20] Dewu 매뉴얼 고객센터 찾는 방법 [2024-03- 20] 듀우 픽업 코드 확인 방법 [2024-03-20] 듀우 구매처 찾기 [2024-03-20] 듀우 VIP 개설 방법 [2024-03-20] 듀우 반품, 교환 신청 방법

Quark Browser는 현재 매우 인기 있는 다기능 브라우저이지만 대부분의 친구들은 Quark Browser의 기능을 사용하는 방법을 모릅니다. 다음으로 편집기가 사용자에게 가장 일반적으로 사용되는 기능과 기술을 정리합니다. . 관심 있는 사용자가 함께 와서 살펴볼 수 있는 Quark Browser의 다기능 사용 튜토리얼을 요약합니다! Quark 브라우저 사용 튜토리얼 [2024-01-09]: 시험지를 스캔하여 Quark에서 답을 보는 방법 [2024-01-09]: Quark 브라우저에서 성인 모드를 활성화하는 방법 [2024-01-09]: 방법 Quark 사용 공간 삭제 [2024-01-09]: Quark 네트워크 디스크 저장 공간 정리 방법 [2024-01-09]: Quark 백업 취소 방법 [2024-01-09]: Quark

numpy 버전 업그레이드 방법: 따라하기 쉬운 튜토리얼, 구체적인 코드 예제 필요 소개: NumPy는 과학 컴퓨팅에 사용되는 중요한 Python 라이브러리입니다. 효율적인 수치 연산을 수행하는 데 사용할 수 있는 강력한 다차원 배열 객체와 일련의 관련 함수를 제공합니다. 새 버전이 출시되면 새로운 기능과 버그 수정이 지속적으로 제공됩니다. 이 문서에서는 설치된 NumPy 라이브러리를 업그레이드하여 최신 기능을 얻고 알려진 문제를 해결하는 방법을 설명합니다. 1단계: 처음에 현재 NumPy 버전을 확인하세요.

1. 먼저 위챗을 엽니다. 2. 오른쪽 상단의 [+]를 클릭하세요. 3. QR코드를 클릭하시면 결제가 진행됩니다. 4. 오른쪽 상단에 있는 세 개의 작은 점을 클릭하세요. 5. 결제도착 음성알림을 클릭하시면 종료됩니다.

여름에 비가 내린 후에는 아름답고 마법 같은 특별한 날씨 장면인 무지개를 자주 볼 수 있습니다. 이 역시 사진에서 볼 수 있는 보기 드문 장면으로, 매우 포토제닉하다. 무지개가 나타나는 데에는 몇 가지 조건이 있습니다. 첫째, 공기 중에 충분한 물방울이 있고, 둘째, 태양이 낮은 각도로 빛납니다. 따라서 비가 그친 후 오후에 무지개를 보는 것이 가장 쉽습니다. 그러나 무지개의 형성은 날씨, 빛, 기타 조건의 영향을 크게 받기 때문에 일반적으로 짧은 시간 동안만 지속되며, 가장 잘 볼 수 있고 촬영할 수 있는 시간은 더욱 짧습니다. 그러면 무지개를 만났을 때 어떻게 제대로 기록하고 고품질로 사진을 찍을 수 있습니까? 1. 무지개를 찾아보세요. 위에서 언급한 조건 외에도 무지개는 대개 햇빛 방향으로 나타납니다. 즉, 태양이 서쪽에서 동쪽으로 빛날 경우 무지개가 동쪽에서 나타날 확률이 높습니다.

PhotoshopCS는 Photoshop Creative Suite의 약자로 Adobe에서 제작한 소프트웨어입니다. 그래픽 디자인 및 이미지 처리에 널리 사용됩니다. PS를 처음 배우는 사용자로서 오늘은 photoshopcs5가 무엇인지, photoshopcs5를 사용하는 방법에 대해 설명하겠습니다. . 1. Photoshop CS5는 어떤 소프트웨어입니까? Adobe Photoshop CS5 Extended는 영화, 비디오 및 멀티미디어 분야의 전문가, 3D 및 애니메이션을 사용하는 그래픽 및 웹 디자이너, 엔지니어링 및 과학 분야의 전문가에게 이상적입니다. 3D 이미지를 렌더링하고 이를 2D 합성 이미지로 병합합니다. 쉽게 비디오 편집

모니터 구입 시 테스트는 파손 구입을 피하기 위해 꼭 필요한 부분입니다. 오늘은 모니터 테스트를 위한 소프트웨어 사용법을 알려드리겠습니다. 방법 단계 1. 먼저 본 사이트에서 DisplayX 소프트웨어를 검색하여 다운로드한 후 설치하고 열면 사용자에게 제공되는 다양한 감지 방법을 확인할 수 있습니다. 2. 사용자는 일반 전체 테스트를 클릭합니다. 첫 번째 단계는 디스플레이의 밝기를 테스트하여 상자가 선명하게 보이도록 조정하는 것입니다. 3. 그런 다음 마우스를 클릭하여 다음 링크를 입력합니다. 모니터가 각 흑백 영역을 구분할 수 있으면 모니터가 여전히 양호하다는 의미입니다. 4. 마우스 왼쪽 버튼을 다시 클릭하면 모니터의 그레이스케일 테스트를 볼 수 있습니다. 색상 전환이 매끄러울수록 모니터가 더 좋아진 것입니다. 5. 또한, 우리는 displayx 소프트웨어에서

스마트폰의 지속적인 발전과 함께 휴대폰의 기능은 점점 더 강력해졌고, 그 중 장사진 촬영 기능은 많은 사용자들이 일상생활에서 사용하는 중요한 기능 중 하나로 자리 잡았다. 긴 스크린샷은 사용자가 쉽게 보고 공유할 수 있도록 긴 웹페이지, 대화 기록, 사진을 한 번에 저장하는 데 도움이 됩니다. 많은 휴대폰 브랜드 중에서 Huawei 휴대폰은 사용자들로부터 높은 평가를 받는 브랜드 중 하나이며, 긴 사진을 자르는 기능도 높은 평가를 받고 있습니다. 이 기사에서는 Huawei 휴대폰으로 장사진을 찍는 올바른 방법과 Huawei 휴대폰을 더 잘 활용하는 데 도움이 되는 몇 가지 전문가 팁을 소개합니다.
